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:

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 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

FieldTypeRequiredDescription
subjectobjectAlwaysIdentifies the user the event is for.
subject.emailstringAlwaysEmail address of the user.
timestampstringAlwaysISO 8601 timestamp, 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.location_idintegerExactly one of target.location_id or target.level_id, when target is suppliedRobin location ID.
target.level_idinteger"Robin level ID.
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 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

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, or its email field is missing or 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.
INVALID_TARGETSomething 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.

emailsubject.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.

lockIdsource.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).
  • 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_EVENTS response for which events were rejected.
  • Source mapping is per-connector: the mapping that resolves source.id values 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.