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:
- Connections and provenance. An
edgesrow or aderivedFromrow to a resource you cannot read stays in the list withreadable: false; aderivativesrow you cannot read is left out. - Computed properties. A
derivedKeysentry whose formula lives on a parent you cannot read names itan enclosing itemand keepsparentResourceId, withreadable: false. - Types. A resource whose type you cannot read reports
resourceTypeName: "a type"and no schema. - Requirements. A requirement whose condition names something you cannot read describes it with the words above.
- Refusal messages name only what you can read. Messages may be reworded at any time
(errors); key on
error, never on the message.
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.