Universal Connector
This API endpoint allows you to send 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, provisioned by Robin during onboarding — see Prerequisites.
Prerequisites
Before you can send events, two things need to be in place. First, you need a connector UUID and an access token. Both are issued by Robin and are not self-served — contact your Robin representative to have a connector provisioned. You'll use the UUID in the endpoint path and the access token in the Authorization header. Second, if your integration will send events using source, you also need a source mapping configured for the connector. This tells Robin which Robin entity each source.id value corresponds to. See Source mapping below. Integrations that only ever send target can skip this step.
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 exactly one of target.location_id or target.level_id. | No. |
The XOR is hierarchical: at the top level, exactly one of source or target must be supplied. When target is supplied, exactly one of target.location_id or target.level_id must be present inside it. Violations are rejected with MALFORMED_EVENTS — the per-event error code identifies which side (INVALID_REQUEST for the top-level XOR, INVALID_TARGET for the inner one). Each event in a batch is validated and routed independently, so a single request can mix source and target events.
Source mapping
When you send events using source, Robin needs to know which Robin entity (a location or a level) each source.id value corresponds to. This mapping is configured per connector by the Robin team and is not something the caller provides at request time.
Onboarding requirement: If your integration will send events using
source, you must coordinate with the Robin team to configure the source mapping for your connector before events will resolve. Without a configured mapping, valid-looking source events have nothing to resolve to and will be rejected. Reach out to your Robin contact during integration onboarding to provision this.
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.
// Currently only `email` is supported; additional identifier fields
// (e.g., employee_id, badge_id, sso_id) may be added over time.
export type Subject = {
email: 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. Must contain exactly one of
// `location_id` or `level_id`. Both are integer Robin IDs.
export type Target = {
location_id?: number; // Robin location (building / site) ID.
level_id?: number; // Robin level (floor) ID.
};
// 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 and a
* deprecation warning is logged. Will be removed in a future 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 and a deprecation warning is
* logged. Will be removed in a future version.
*/
lockId?: string;
};Field reference
| Field | Type | Required | Description |
|---|---|---|---|
subject | object | Always | Identifies the user the event is for. |
subject.email | string | Always | Email address of the user. |
timestamp | string | Always | ISO 8601 timestamp, 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.location_id | integer | Exactly one of target.location_id or target.level_id, when target is supplied | Robin location ID. |
target.level_id | integer | " | Robin level ID. |
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 event — Robin location
{
"events": [
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-05-18T15:30:00Z",
"target": { "location_id": 1234567890 }
}
]
}Target event — Robin level
{
"events": [
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-05-18T15:30:00Z",
"target": { "level_id": 9876543210 }
}
]
}Mixed batch
A single request can mix source and target events; each event is validated and routed independently.
{
"events": [
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-05-18T15:30:00Z",
"source": { "id": "lock-1234" }
},
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-05-18T15:32:00Z",
"target": { "location_id": 1234567890 }
},
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-05-18T15:35:00Z",
"target": { "level_id": 9876543210 }
}
]
}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
{
"status": 200,
"body": {
"code": "OK"
}
}Error responses
400 Bad Request — returned if the connector uuid is misformatted, the payload has already been processed, or one or more events are malformed.
{
"status": 400,
"body": {
"code": "INVALID_UUID_FORMAT" | "PAYLOAD_ALREADY_PROCESSED" | "MALFORMED_EVENTS"
}
}403 Forbidden — returned if the access token is missing or invalid, or the organization integration is missing permissions.
{
"status": 403,
"body": {
"code": "MISSING_ACCESS_TOKEN" | "INVALID_ACCESS_TOKEN" | "NOT_PERMITTED"
}
}404 Not Found — returned if the organization integration status is invalid.
{
"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, or its email field is missing or 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. |
INVALID_TARGET | Something is wrong with target — it is not an object, contains unsupported keys, violates the inner XOR (zero or both of location_id / level_id), or refers to an ID that doesn't exist or doesn't belong to the caller'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 and a deprecation warning is logged per request, scoped to the connector UUID, so you can identify which integrations still need migration. Both legacy fields will be removed in a future major version.
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). - Per-event validation: every event in a batch is validated independently. A single malformed event does not invalidate the rest of the batch — see the
MALFORMED_EVENTSresponse for which events were rejected. - Source mapping is per-connector: the mapping that resolves
source.idvalues to Robin entities is configured on the connector itself by the Robin team. There is no caller-side way to inspect or modify it through this endpoint. - Rate limiting: check your API rate limits to avoid hitting the maximum number of requests per minute.
Updated 6 days ago
