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.

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 two scope sets

Set Grants Applies to
read_types list and fetch GET doors
operate_types change PATCH, enrol, exit, attest, status

Operating implies reading. A type in operate_types does not need repeating in read_types; the readable set is the union of the two.

Which endpoints need which:

A third list, 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.

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.

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 kiosk stations use — refuses an integration key. (Kiosk station credentials are a separate plane, not covered by this pack.) 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 kiosk station 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/13813"

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.