Authentication and scopes

Sending the key

Every request carries the key as a bearer token:

Authorization: Bearer <your key>

There is no other accepted form — no query parameter, no custom header, no cookie. The key is shown once, when it is created. It is not recoverable afterwards; if it is lost, a new one is issued.

People sign in; apps hold credentials

A key (a credential) is your app's own identity, for work no person is doing. When a person uses your app, they can sign in with their ReSync account instead, and your app then acts as them, with their access and never more than the app is registered for — see sign-in.md. Both are sent the same way, as the bearer token.

What a key is bound to

A key belongs to exactly one organization, called its owner. That binding is fixed for the life of the key: it cannot be moved to another organization, and a key cannot be made to act for two.

Everything the key can reach is owned by that organization. There is no shared or public pool of resources, so a resource belonging to another organization is refused whether you reach for it by id, by list, or through a related record.

The scope sets

A key's scopes are lists. Three type sets — read_types, operate_types and attest_types — govern most doors (the table below). find_or_create_types is a fourth type set that governs one door, and bind_systems lists external systems, not types.

Set Grants Applies to
read_types list and fetch GET doors
operate_types change PATCH, enrol, exit, attest, status
attest_types record and advance, never change attest, status — and the GET doors

Operating implies reading, and so does attesting. A type in operate_types or attest_types does not need repeating in read_types; the readable set is the union of the three. attest_types lets a key record attestations and move status on a type without being able to edit, enrol or exit anything of that type (see Attesting without changing).

Which endpoints need which:

find_or_create_types governs POST /v1/resources/find-or-create only. It is separate on purpose: being able to change a bid is not the same as being able to bring one into existence.

The general-reference lists — types, statuses, lifecycles, units and so on — are not type-scoped. Any valid key may read them. A by-id read of one of them (a type, a lifecycle, its graph, its transitions) answers 404 for anything the key's own list would not show.

POST /v1/ingest also authenticates an API key. Its contract is not yet part of this pack.

Attesting without changing

attest_types is a list of types on which the key may record attestations and move status, and nothing more. It reads them too. It does not let the key edit, enrol or exit: those still need operate_types, and a key without it is refused 403 operate_types_required. Use it for a client whose job is to report what happened — a scanner, a checklist app — rather than to change records.

Device credentials

A device credential is a key that acts at one resource — a workstation, a counter, a door. An administrator at the owning organization issues it like any key, with up to three more scope entries:

Scope Meaning
acts_at_resource_id The resource the device acts at. The key may read it and everything inside it, and record attestations and move status there. It never edits, enrols or exits.
actor_source presence_session — the device acts for whoever has checked in at it, never for itself.
presence_session_ttl_s Seconds of inactivity before a check-in ends: 30–3600, 300 when absent.

A device credential may also hold the other scope sets; attest_types is the usual one, for types it records on outside its own resource.

Checking people in. A device credential with actor_source opens a presence session when a person presents an identifier to the device (endpoints): POST /v1/presence-sessions with the identifier's system and code. One person is checked in at a time; a different person's check-in ends the previous one. The session ends on its own after presence_session_ttl_s seconds without activity; every write made under it, and POST /v1/presence-sessions/current/touch, counts as activity.

Acting for the person. Every write by a device credential with actor_source must carry the presenceSessionId it received at check-in. The write is recorded as that person's act. It cannot name an actor any other way: actorResourceId and actorAlias are refused (403 actor_source_presence_session_only), a missing id is 400 presence_session_required, and an ended or expired session is 401 presence_session_expired — check the person in again. The id is returned as a string; a string or a number is accepted back.

Linking an identifier. An administrator issues a one-time enrolment code for a person in ReSync; the person presents the code and the identifier to the device, which calls POST /v1/alias-enrollments/redeem. The enrolment must belong to the key's own organization.

Live updates. A device credential may hold the live-update stream, GET /v1/live — for its own resource and what it can read. Any other key is refused (below).

What it checks every time. On every request a device credential's resource must still exist (403 acts_at_resource_missing) and still belong to the key's organization (403 acts_at_outside_owner_grant).

GET /v1/api-keys/self returns these entries inside scopes, and actsAt names the resource:

From device-self.json — status 200:

{
  "apiKeyId": 785,
  "name": "device-capture",
  "scopes": {
    "actor_source": "presence_session",
    "attest_types": [
      "person"
    ],
    "acts_at_resource_id": 24914,
    "presence_session_ttl_s": 300,
    "created_owner_resource_id": 5
  },
  "actsAt": {
    "resourceId": 24914,
    "name": "Capture Station"
  },
  "owner": {
    "resourceId": 5,
    "name": "Popular Design Mill & Cabinet, Inc."
  }
}

Telling ReSync which device. A device credential's app may send x-device-id on its requests — an id the app makes once per install (letters, digits, ., _, : and -; up to 128 characters). The first request from a new device is recorded on the credential's history with the device's user-agent, so an administrator can see where a credential is in use. Nothing else changes; a request without the header works the same.

Changing scopes

You cannot change your own scopes. An administrator at the owning organization does it, through a surface your key cannot reach. Ask them; do not build a retry around a 403.

Every scope change is recorded on the ledger, with the complete before and after. So is the creation of the key and its revocation. If you need to know why a call that worked last week fails today, that record is the answer, and your administrator can read it.

Revocation

A key is revoked, not deleted. Once revoked it stops authenticating immediately and permanently; there is no un-revoke. Everything it wrote stays on the ledger and stays attributed to it.

Keys do not expire on their own. A key is valid until somebody revokes it — so treat it like a password that never rotates itself, and ask for a new one if you suspect it has leaked.

A machine key that belongs to a connected app (sign-in.md) is rotated by the organization's administrator from the app's page: a new key under a new name is minted and shown once, then the old key is revoked. Revoking the app revokes every key that belongs to it.

Rate limit

300 requests per minute per key by default. The budget is counted per credential, so another integration cannot spend yours and you cannot spend theirs. ReSync may set a different limit for your key — for a large integration, ask your administrator; it is not something a key can change for itself. Whatever your key's limit is, every response says so:

x-ratelimit-limit: 300
x-ratelimit-remaining: 297
x-ratelimit-reset: 59

The window is fixed, not rolling: it starts with your first request and lasts one minute; when it ends, the whole budget is back. x-ratelimit-reset is the seconds left in the current window.

When you exhaust the budget the answer is 429, with retry-after (seconds left in the window) beside the same headers. Captured on a key whose own limit is 3, at its fourth request:

x-ratelimit-limit: 3
x-ratelimit-remaining: 0
x-ratelimit-reset: 60
retry-after: 60

From rate-limited-429.json — status 429:

{
  "error": "rate_limited",
  "message": "rate limit exceeded — 3 requests per 1 minute",
  "retryAfterSeconds": 60
}

message reads "… requests per

What a key cannot do yet

Hold a live stream. GET /v1/live — the live-update stream ReSync's web app and device credentials use — refuses an integration key (device credentials may hold it). Poll instead (events.md).

From live-key-not-supported.json — status 403:

{
  "error": "live_key_not_supported",
  "message": "Live updates are available to sessions and device credentials only; poll the events read with ?after= instead"
}

Request ids

Every response carries x-request-id.

If you send one, it is echoed back, so you can use your own correlation id end to end:

curl -s -H "Authorization: Bearer $TOKEN" -H "x-request-id: order-sync-8842" "$BASE/v1/resources/<customerId>"

Ids are sanitised: letters, digits, ., _, : and - survive, everything else is stripped, and the result is truncated to 64 characters. If you send nothing, one is generated for you.

Quote it when you report a problem. It is how a specific request of yours is found in the server logs.