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:
- A connector UUID and an access token. A Robin admin creates both in the Robin dashboard. See Get your connector UUID and access token.
- A source mapping, if your integration sends events with
source. The mapping tells Robin which Robin location eachsource.idvalue belongs to. A Robin admin adds it in the dashboard. See Source mapping. Integrations that only sendtargetcan 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:
- In the Robin dashboard, go to Manage > Integrations.
- In the Access Control card, find Universal Presence Connector and select Manage.
- Select New Connection, then select Universal Presence Connector.
- Enter an Integration name and select a Vendor. To test without creating real presence, turn on Test Mode. You cannot change the mode later.
- Select Create connection.
- 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:
- In the Robin dashboard, go to Manage > Integrations.
- In the API tokens card, select Generate new token.
- 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:
| Locator | What it is | Requires Robin-side mapping? |
|---|---|---|
source | A reference to an entity in your system (a lock, reader, device, etc.). Holds a single field, source.id. | Yes — see Source mapping. |
target | A 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
| Field | Type | Required | Description |
|---|---|---|---|
subject | object | Always | Identifies the user the event is for. |
subject.email | string | Exactly one of subject.email or subject.scim_user_id | The user's primary email address in Robin. Case-insensitive. |
subject.scim_user_id | string | Exactly one of subject.email or subject.scim_user_id | The user's employeeNumber from the SCIM enterprise extension, as your identity provider sends it to Robin. Case-sensitive. |
timestamp | string | Always | ISO 8601 timestamp with a UTC offset, e.g. 2026-05-18T15:30:00Z. |
source | object | Exactly one of source or target | Reference to an entity in your system. Resolved via the connector's source mapping. |
source.id | string | When source is supplied | Identifier in your system. |
target | object | Exactly one of source or target | Direct reference to a Robin entity. |
target.id | integer | When target is supplied | Robin entity ID (positive integer). |
target.type | enum | When target is supplied | "LOCATION" (building / site) or "LEVEL" (floor within a location). |
email | string | Never (deprecated) | Legacy top-level alias of subject.email. |
lockId | string | Never (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"
}
}| Code | Returned when |
|---|---|
INVALID_UUID_FORMAT | The connector uuid path parameter is not a well-formed UUID. |
INVALID_PAYLOAD | The 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_EVENTS | The 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
| Code | Meaning |
|---|---|
INVALID_REQUEST | Event is missing required keys, contains unsupported top-level keys, or violates the locator XOR (zero or both of source / target). |
INVALID_SUBJECT | Something 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_TIMESTAMP | timestamp is not a supported ISO 8601 value. |
INVALID_SOURCE | Something 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_PROCESSED | An 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_TARGET | target 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
email → subject.emailIf your integration currently sends a top-level email:
- No urgent action required. Existing payloads continue to work; a bare top-level
emailis treated assubject.email. - When you next touch the integration, move the value into a
subjectobject as theemailfield. The value itself is unchanged.
lockId → source.id
lockId → source.idIf your integration currently sends lockId:
- No urgent action required. Existing payloads continue to work; a bare top-level
lockIdis treated assource.id. - When you next touch the integration, move the value into a
sourceobject as theidfield. 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_EVENTSresponse 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
LEVELtarget 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 withINVALID_EMAILon the Processed Events tab. - Request size: the body limit is 10 MB.
Updated 19 days ago