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, 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 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. 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. 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 | 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.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; each event is validated and routed independently.
{
"events": [
{
"subject": { "email": "[email protected]" },
"timestamp": "2026-06-01T09:15:00Z",
"source": { "id": "lock-1234" }
},
{
"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). Events not present in failedEvents were accepted. Note that there is currently no way for callers to supply their own per-event identifier on the way in — use row to correlate failures back to the submitted batch.
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 | 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). - 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.
