Conventions

The rules that hold everywhere, and the limits worth knowing before you design around them.

Shapes

Ids are strings. resourceId, resourceTypeId and similar come back as strings because some exceed what a JavaScript number holds exactly. Keep them as strings; do not parse them to compare.

Some nested fields are numbers rather than strings — ownerResourceId in a list row, for instance. This is an inconsistency, not a signal. Compare ids as strings after coercing, and it will not bite you.

ownerOrgId on a single-resource read is an internal identifier and may change; use ownerResourceId, which is the owning resource's id.

Timestamps are ISO-8601 in UTC, e.g. 2026-09-14T02:00:12.772Z. Always UTC; there is no local time anywhere in the API.

Fields are camelCase, with two exceptions that predate the rest of the surface and are called out where they appear: POST /v1/resources/find-or-create and POST /v1/aliases/bind take snake_case bodies.

Cursors are opaque. Pass back exactly what you were given.

Unknown query parameters are ignored, not rejected — a misspelt parameter does nothing and the request answers as if it were absent. Check names against endpoints.md. (A parameter a door knows but cannot apply to a key, such as sort on the resource list, is refused with a code.)

Paging

limit is clamped to 1–100 everywhere. Defaults differ: the resource list defaults to 50, the events read to 30. Always send an explicit limit and the difference stops mattering.

Writing properties

properties merges shallowly. Keys you do not send are left alone. Nested objects are replaced wholesale, not merged — send the complete nested value.

You cannot delete a property key. Sending {"colour": null} stores a null; it does not remove colour. There is currently no way through this API to make a key not exist again.

Properties you cannot write

Two kinds of property refuse your writes, and both refuse the entire request rather than applying part of it. A patch either lands completely or not at all.

Confirmed properties — a person has vouched for the value. 409 confirmed_key_locked, with keys naming which. A person must re-confirm before it can change; your key cannot overturn somebody's assertion. Read confirmedKeys on a resource, or add include=properties to a list, to see which are locked before you try.

Derived properties — computed by a formula from other data. 400 derive_target_write_blocked, with keys naming them and where each is computed. A manual value would be overwritten the next time the formula runs, so the write is refused instead of silently lost. derivedKeys on a resource read tells you which they are.

If one property is both, you get the derived refusal.

Names you cannot read

A response never tells you the name of something your key cannot read. Where a field would name it, you get a neutral word instead, and the field keeps its type — a name is still a string:

What the field names The word you get
a person, an organization, an actor, a counterparty someone
any other resource a resource
a resource type a type
something above the resource (a parent, a setting's source) an enclosing item
something inside the resource an item inside
a setting's value a setting

Ids are not secret. The id beside a hidden name is still there, so you can tell two hidden things apart, and a row that names something you cannot read carries readable: false. You can always rely on that flag; never compare a name against one of the words above.

What this means in practice:

Reading efficiently

List rows omit properties by default. Add include=properties and each row gains properties and confirmedKeys — one request instead of one per row. Do that rather than listing ids and fetching each one; the difference is large over a few hundred rows.

Reference data (types, statuses, lifecycles, units) changes rarely. Fetch it at startup and keep it.


What you can't do yet

Said plainly, so you can design around it rather than discover it.

Writes are not idempotent. There is no Idempotency-Key. A retried PATCH writes its events a second time, and a request that timed out may have succeeded. Read the resource back before resending a write. The one exception is POST /v1/resources/find-or-create, which is idempotent by your own identifier — for anything you create, prefer it.

You cannot delete a property key. See above.

Some surfaces are not open to keys. A resource's canvas placement, its explanation (/why), the compatibility candidates and every authoring surface (types, lifecycles, compatibility markings, retyping a connection) require a signed-in person; 401 or 403 there is the current design, not a misconfiguration. A key reaches a resource's members, containers, events, advancement, requirements and ports (with legal targets), the cross-type search, and it can enrol, exit and move members and make or remove an attachment — see endpoints.md for every door.

You cannot manage your own key. Creating, rescoping and revoking are administrator actions through the web application. Your key cannot even read the list of keys — only GET /v1/api-keys/self.

Keys do not expire. A key stays valid until somebody revokes it. Plan your own rotation.

Authentication failures come back in two different shapes. On most endpoints a bad or revoked key returns { statusCode, error, message } with a phrase in error, not a code. Treat any 401 as "check your key" — see errors.md.

A few doors return sentences rather than codes for some of their own validation — attestations and status changes among them. errors.md lists every one. Match on the HTTP status there.

There is no webhook or push for keys. To notice a change you poll a resource's events with after and the cursor from your last poll (events.md). ReSync's own web app and kiosk stations receive live updates over a stream; that stream is not open to integration keys.

There is no bulk write. One resource per request.