Universal Presence Connector

This API endpoint allows you to send presence and badge events to a specific connector identified by its unique UUID. Each event contains user-related data including email, timestamp, and a lock identifier.

Universal Connector — Presence Events

Send presence events from a third-party system (badge readers, smart locks, turnstiles, occupancy sensors, etc.) into Robin via the universal connector.

POST https://badge-presence-collector.services.robinpowered.com/v1.0/connectors/{uuid}/presence

The {uuid} path parameter is your connector's unique identifier. An admin creates the connector in the Robin dashboard — see Get your connector UUID and access token.

For organizations hosted in the EU, use https://badge-presence-collector.services.robinpowered.eu as the base URL.


Prerequisites

Before you can send events, you need two things:

  1. A connector UUID and an access token. A Robin admin creates both in the Robin dashboard. See Get your connector UUID and access token.
  2. A source mapping, if your integration sends events with source. The mapping tells Robin which Robin location each source.id value belongs to. A Robin admin adds it in the dashboard. See Source mapping. Integrations that only send target can skip this step.

For the admin's step-by-step guide, see Set up the Universal Presence Connector in the Robin help center.

Get your connector UUID and access token

A Robin user with the Admin or Owner role (or another role with the Badge integrations permission) does these steps.

Create the connector and copy its UUID:

  1. In the Robin dashboard, go to Manage > Integrations.
  2. In the Access Control card, find Universal Presence Connector and select Manage.
  3. Select New Connection, then select Universal Presence Connector.
  4. Enter an Integration name and select a Vendor. To test without creating real presence, turn on Test Mode. You cannot change the mode later.
  5. Select Create connection.
  6. On the Connector added screen, select Copy next to Connection ID. This is the connector UUID.

To find the UUID later, select the gear next to the connection on the Universal Presence Connector page. The details panel shows it as Integration ID. The Webhook URL in the same panel is the full endpoint URL, with the UUID already in the path.

Create an access token:

  1. In the Robin dashboard, go to Manage > Integrations.
  2. In the API tokens card, select Generate new token.
  3. Name the token and generate it. Copy the token and store it as a secret.

The token must belong to the same Robin organization as the connector, and it must have the badge_integrations:manage permission in that organization. An organization API token has it. A personal token of an Admin or Owner also works, but it stops working if that person loses the role or leaves the organization, so prefer an organization token.

Test mode

A connector in test mode accepts the same requests and runs the same validation and mapping as a live connector, but it creates no presence in Robin. Test events show as Test successful on the connector's Processed Events tab in the dashboard. To go live, the admin creates a second connector with Test Mode off, and you send to its UUID.


Identifying where the event happened

Every event carries a single locator that tells Robin where the presence happened. There are two kinds of locator, and you provide exactly one of them per event:

LocatorWhat it isRequires Robin-side mapping?
sourceA reference to an entity in your system (a lock, reader, device, etc.). Holds a single field, source.id.Yes — see Source mapping.
targetA direct reference to a Robin entity. Holds target.id (the integer Robin ID) and target.type ("LOCATION" or "LEVEL").No.

Exactly one of source or target must be supplied per event. Violations are rejected with MALFORMED_EVENTS and INVALID_REQUEST on the offending event. A single request can mix source and target events.

Source mapping

When you send events using source, Robin needs to know which Robin location (and, optionally, level) each source.id value belongs to. A Robin admin configures this per connector in the dashboard, on the connector's Mappings tab. You do not send the mapping in the request.

Each mapping has a Source Pattern. A pattern can be an exact value, such as ATL-Front-Door, or use * (any number of characters) and ? (one character), such as ATL*. Matching is case-insensitive. When more than one pattern matches, an exact match wins, then the most specific pattern.

A source event whose source.id matches no mapping is accepted, then fails with LOCK_MAPPING_MISSING. The admin sees it on the connector's Processed Events tab.


Request payload

The request body is JSON conforming to the type definitions below.

Type definitions

export type ConnectorPayload = {
  events: ConnectorEvent[];
};

// Identifies the user the event is for. Supply exactly one of
// `email` or `scim_user_id`.
export type Subject =
  | { email: string }
  | { scim_user_id: string };

// Reference to an entity in the caller's system. Resolved to a Robin
// entity via the connector's source mapping.
export type Source = {
  id: string;
};

// Direct reference to a Robin entity. Both fields are required.
export type Target = {
  id: number;                  // Integer Robin entity ID.
  type: "LOCATION" | "LEVEL";  // Which kind of entity `id` refers to.
};

// Each event must include `subject` and `timestamp`, plus
// exactly one of `source` or `target`.
export type ConnectorEvent = {
  subject: Subject;
  timestamp: string;

  source?: Source;
  target?: Target;

  /**
   * @deprecated Use `subject.email` instead. Continues to be accepted for
   * backward compatibility — a bare top-level `email` is treated as
   * `subject.email`. If both are supplied, `subject.email` wins. Will be
   * removed in a future major version.
   */
  email?: string;

  /**
   * @deprecated Use `source.id` instead. Continues to be accepted for backward
   * compatibility — a bare `lockId` is treated as `source.id`. If both `lockId`
   * and `source` are supplied, `source` wins. Will be removed in a future
   * major version.
   */
  lockId?: string;
};

Field reference

FieldTypeRequiredDescription
subjectobjectAlwaysIdentifies the user the event is for.
subject.emailstringExactly one of subject.email or subject.scim_user_idThe user's primary email address in Robin. Case-insensitive.
subject.scim_user_idstringExactly one of subject.email or subject.scim_user_idThe user's employeeNumber from the SCIM enterprise extension, as your identity provider sends it to Robin. Case-sensitive.
timestampstringAlwaysISO 8601 timestamp with a UTC offset, e.g. 2026-05-18T15:30:00Z.
sourceobjectExactly one of source or targetReference to an entity in your system. Resolved via the connector's source mapping.
source.idstringWhen source is suppliedIdentifier in your system.
targetobjectExactly one of source or targetDirect reference to a Robin entity.
target.idintegerWhen target is suppliedRobin entity ID (positive integer).
target.typeenumWhen target is supplied"LOCATION" (building / site) or "LEVEL" (floor within a location).
emailstringNever (deprecated)Legacy top-level alias of subject.email.
lockIdstringNever (deprecated)Legacy alias of source.id.

Request headers

Send Content-Type: application/json and Authorization: Access-Token YOUR_ACCESS_TOKEN.


Examples

Source event

{
  "events": [
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-05-18T15:30:00Z",
      "source": { "id": "lock-1234" }
    }
  ]
}

Target events

target.type selects whether the id refers to a Robin location or level.

{
  "events": [
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-06-01T09:15:00Z",
      "target": { "id": 999, "type": "LOCATION" }
    },
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-06-01T09:16:00Z",
      "target": { "id": 555, "type": "LEVEL" }
    }
  ]
}

Mixed batch

A single request can mix source and target events. If any event in the batch is invalid, Robin rejects the whole batch — see Error responses.

{
  "events": [
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-06-01T09:15:00Z",
      "source": { "id": "lock-1234" }
    },
    {
      "subject": { "scim_user_id": "E123456" },
      "timestamp": "2026-06-01T09:15:00Z",
      "target": { "id": 123, "type": "LOCATION" }
    },
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-06-01T09:16:00Z",
      "target": { "id": 999, "type": "LOCATION" }
    },
    {
      "subject": { "email": "[email protected]" },
      "timestamp": "2026-06-01T09:17:00Z",
      "target": { "id": 555, "type": "LEVEL" }
    }
  ]
}

cURL

curl -X POST "https://badge-presence-collector.services.robinpowered.com/v1.0/connectors/YOUR_CONNECTOR_UUID/presence" \
     -H "Content-Type: application/json" \
     -H "Authorization: Access-Token YOUR_ACCESS_TOKEN" \
     -d '{
           "events": [
             {
               "subject": { "email": "[email protected]" },
               "timestamp": "2026-05-18T15:30:00Z",
               "source": { "id": "lock-1234" }
             }
           ]
         }'

Responses

Success

A successful response returns HTTP 200 with one of two codes:

{
  "status": 200,
  "body": {
    "code": "OK" | "PAYLOAD_ALREADY_PROCESSED"
  }
}

PAYLOAD_ALREADY_PROCESSED is returned when the same payload has already been ingested (idempotency hit). Treat it as success — no retry needed.

Error responses

400 Bad Request — returned with one of the following codes:

{
  "status": 400,
  "body": {
    "code": "INVALID_UUID_FORMAT" | "INVALID_PAYLOAD" | "MALFORMED_EVENTS"
  }
}
CodeReturned when
INVALID_UUID_FORMATThe connector uuid path parameter is not a well-formed UUID.
INVALID_PAYLOADThe request body itself is unusable. Specifically: the body is not a JSON object; the body is a JSON object but has no events field; events is present but is not an array (e.g., a string, number, object, or null); or the request was sent without Content-Type: application/json, in which case the server sees an empty {} body.
MALFORMED_EVENTSThe body parsed cleanly and events is an array, but one or more individual events failed validation. See the structured response below.

When the code is MALFORMED_EVENTS, the body additionally includes a failedEvents array detailing which events were rejected and why:

{
  "code": "MALFORMED_EVENTS",
  "failedEvents": [
    {
      "row": 0,
      "eventUuid": "f1c2a3...",
      "reason": ["INVALID_SUBJECT", "INVALID_TIMESTAMP"]
    }
  ]
}

Each entry has the event's zero-based row in the submitted batch, a server-assigned eventUuid for log correlation, and a reason array listing every per-event error code that applies (so a single event can surface multiple failures in one response).

A batch is all or nothing. When the response is MALFORMED_EVENTS, Robin accepts none of the events in the batch, including events not listed in failedEvents. Fix or remove the failed events and send the batch again. Callers cannot supply their own per-event identifier — use row to correlate failures back to the submitted batch.

403 Forbidden — returned if the access token is missing or malformed, the token lacks badge_integrations:manage in the connector's organization, or an admin turned off the connector (the Enabled switch in the dashboard).

{
  "status": 403,
  "body": {
    "code": "MISSING_ACCESS_TOKEN" | "INVALID_ACCESS_TOKEN" | "NOT_PERMITTED" | "INTEGRATION_DISABLED"
  }
}

404 Not Found — returned if no connector exists with this UUID, for example because an admin deleted it.

{
  "status": 404,
  "body": {
    "code": "INVALID_INTEGRATION_STATUS"
  }
}

Malformed event codes

CodeMeaning
INVALID_REQUESTEvent is missing required keys, contains unsupported top-level keys, or violates the locator XOR (zero or both of source / target).
INVALID_SUBJECTSomething is wrong with subject: it is missing, is not an object, contains unsupported keys, supplies neither or both of email / scim_user_id , or the supplied identifier is malformed. The response message specifies the exact cause.
INVALID_TIMESTAMPtimestamp is not a supported ISO 8601 value.
INVALID_SOURCESomething is wrong with source — it is not an object, contains unsupported keys, or its id field is missing or not a string. The response message specifies the exact cause.
ALREADY_PROCESSEDAn event with the same subject, timestamp and locator was already accepted on this connector, in an earlier request or earlier in the same batch.
INVALID_TARGETtarget is not an object, has unsupported keys, target.id is missing or not a positive integer, target.type is missing or not "LOCATION" / "LEVEL", or the entity referenced by target.id does not exist or is not owned by the connector's tenant. The response message specifies the exact cause.

Migrations

Two fields have been renamed in this revision. Existing integrations continue to work — both legacy fields are accepted as aliases on every request. Both legacy fields will be removed in a future major version, so migrate when convenient.

email → subject.email

If your integration currently sends a top-level email:

  • No urgent action required. Existing payloads continue to work; a bare top-level email is treated as subject.email.
  • When you next touch the integration, move the value into a subject object as the email field. The value itself is unchanged.

lockId → source.id

If your integration currently sends lockId:

  • No urgent action required. Existing payloads continue to work; a bare top-level lockId is treated as source.id.
  • When you next touch the integration, move the value into a source object as the id field. The value itself is unchanged.

Additional notes

  • Timestamp format: ISO 8601 (YYYY-MM-DDTHH:mm:ssZ). Always include a UTC offset. Robin records the visit on that day in the building's time zone.
  • Batches are all or nothing: one malformed event rejects the whole batch — see the MALFORMED_EVENTS response for which events failed.
  • Source mapping is per connector: a Robin admin manages it on the connector's Mappings tab in the dashboard. You cannot inspect or change it through this endpoint.
  • What an event does: Robin records a confirmed office visit for the user in that building, for that day. Presence is per building, so a LEVEL target or a mapping to a level records the level's building. Robin does not create users. An email that is not an active user's primary email fails with INVALID_EMAIL on the Processed Events tab.
  • Request size: the body limit is 10 MB.