Errors
The envelope
Errors carry a machine-readable code, an optional human sentence, and sometimes structured detail:
{ "error": "type_not_operable" }
{
"error": "invalid_include",
"message": "unknown include member(s): foo (accepted: properties)"
}
Branch on error. It is a stable string and will not change within v1. message is for your logs and
your support tickets; it may be reworded at any time. Extra fields (keys, requirements,
retryAfterSeconds, item) carry detail specific to that code. item appears only when a
find-or-create that carries fields or children is refused: it names the part of your body that failed —
{ "path": "children[0].children[2]", "external_key": "…" } (no path for the top resource) — beside that part's
own error and message.
Two shapes you will also meet
Authentication failures on most endpoints return a different shape:
{ "statusCode": 401, "error": "Unauthorized", "message": "Invalid token" }
Here error is a phrase, not a code. This happens because those endpoints accept either a key or a
signed-in person, and the rejection is raised before the key layer sees it. A missing key and a malformed
or unknown key are indistinguishable this way.
A revoked key is the exception. If the value you send is a key that has been revoked, these endpoints
return the normal envelope with invalid_api_key, so you can tell a revoked credential from a typo:
From dual-auth-revoked-key.json — status 401:
{
"error": "invalid_api_key",
"message": "This api key has been revoked"
}
Treat any 401 as "check your key" and do not try to parse it further. The key-only endpoints
(/v1/api-keys/self, /v1/aliases/*, /v1/resources/find-or-create, /v1/presence-sessions*,
/v1/alias-enrollments/redeem) return the normal envelope with error: "invalid_api_key".
Some doors return sentences instead of codes for their own validation. Their scope refusals
(403 subject_outside_grant, type_not_readable, type_not_operable …) are always proper codes, but these
checks put an English sentence in error:
POST /v1/resources/:id/attestations— an unknowneventType, a non-flatpayload, a missing declared field;PATCH /v1/resources/:id/status— an illegal move, for example"Resource is already in this status"or"Transition blocked by unmet requirements"(readrequirementsin the422body for the detail);GET /v1/state-transitions— a missingfromStatusId, or neitherresourceIdnorlifecycleId;GET /v1/resources/:id/requirements— a missing or malformedtransitionId;GET /v1/resources/:id/members— a malformed id;GET /v1/resource-types/:id— a malformed id, or a type your key's own list does not show (404);POST /v1/identity-claims/redeem— a missing code oractorAlias, an unknown or expired code, an account that cannot be linked;- the actor fields on every write that accepts them (
PATCH, members, exit, move, attestations, status, find-or-create) — for example"actorResourceId and actorAlias are mutually exclusive"or"actorResourceId must reference an existing person resource".
Match on the HTTP status for those, not on the string.
Every code
Authentication — fix your credential
| Code | Status | Meaning | What to do |
|---|---|---|---|
invalid_api_key |
401 | No key, unknown key, or a revoked key. On endpoints that also accept a signed-in person, only a revoked key returns this code; a missing or unknown one returns the phrase shape above. | Check the header. If it was working, ask whether the key was revoked. Do not retry. |
missing_bearer_token |
401 | No Authorization header at all. |
Fix the request. |
Scope — ask your administrator
These mean your key is working correctly and is not permitted to do this. Retrying will not help.
| Code | Status | Meaning | What to do |
|---|---|---|---|
owner_grant_required |
403 | Your key has no owning organization. Any door that scopes to it can return this — the lists, the per-resource reads and writes, and find-or-create, which would otherwise have no organization to create the resource in. |
Ask for a correctly provisioned key. |
read_types_required |
403 | Your key has no readable types at all. | Ask for read scope. |
operate_types_required |
403 | Your key has no writable types at all. | Ask for operate scope. |
type_not_readable |
403 | This type is in neither of your sets. | Ask for it to be added to read_types. |
type_not_operable |
403 | You may read this type but not change it. | Ask for it to be moved to operate_types. |
subject_outside_grant |
403 | The resource belongs to another organization. | Stop. This is the tenancy boundary; it is not a scope you can be granted. |
verb_not_granted |
403 | You may reach this resource, but not with this kind of action. A refusal that comes from your key's own scopes is always reported as type_not_readable, type_not_operable or outside_acts_at_scope; this code is returned only when the limit is on the side of the person who provisioned your key — they reach the resource through access granted to them directly, or through a connection shared by another organization, and that access does not include this action (or no longer does). A key never holds more than its provisioner. |
Ask for the action to be granted to the person who provisioned the key. |
personal_modeling_off |
403 | The resource is owned by a person rather than an organization, and this instance has personal modeling turned off: anything a person owns can be read, but not changed. A person's own record is not affected. | Stop. Work on resources your organization owns. |
key_unbound |
403 | Your key has not been bound to an acting identity. Only a key provisioned outside the normal path can be in this state. | Ask your administrator to re-provision the key. |
actor_not_readable |
403 | You named an acting person (actorResourceId) your key cannot read. A key may only attribute its action to someone within its reach. |
Send an actorAlias instead, or leave the actor out — the action is then attributed to your key. |
live_key_not_supported |
403 | Live updates (GET /v1/live) are held by signed-in people and device credentials only; an integration key cannot open a stream. |
Poll the resource's events instead (GET /v1/resources/:id/events) with a stored cursor. |
owner_forced_to_grant |
403 | You passed an ownerResourceId that is not yours. |
Drop the parameter — the filter is applied for you. |
outside_acts_at_scope |
403 | Your key is a device credential (it acts at one resource, such as a workstation) and the resource is not at that place. An integration key never receives this. | Act on a resource at the station, or use an integration key. |
Request — fix the request
| Code | Status | Meaning |
|---|---|---|
invalid_resource_id |
400 | The id in the path is not a number. |
invalid_type_id |
400 | typeId is not a number. |
invalid_status_id |
400 | statusId is not a number. |
invalid_owner_resource_id |
400 | ownerResourceId is not a number. |
invalid_cursor |
400 | cursor is not a cursor this API issued. |
invalid_tag_id |
400 | A tagId on GET /v1/resources is not a number. Every repeated value is checked; one bad value refuses the request. |
sort_not_supported |
400 | GET /v1/resources with sort from a key. A key list is always in resourceId order, because its cursor is the last resourceId and could not resume a name or date order exactly. message says so. Drop sort and order the rows yourself. |
invalid_include |
400 | An include member that does not exist. message lists what is accepted. |
type_required |
400 | A list without typeKey or typeId. |
child_resource_id_required |
400 | Enrolment without childResourceId. |
container_id_required |
400 | Exit without containerId, or a move without both fromContainerId and toContainerId (each a positive integer). |
invalid_component_role_id |
400 | componentRoleId is not a positive integer. |
self_membership |
400 | A resource cannot contain itself. |
role_type_mismatch |
400 | That role belongs to a different type than the container. |
system_and_code_required |
400 | Alias lookup without system and code. |
unknown_external_system |
400 | That external system is not registered. |
source_resource_id_required |
400 | A connection without sourceResourceId. |
target_resource_id_required |
400 | A connection without targetResourceId. |
interaction_type_id_required |
400 | A connection without interactionTypeId. |
self_interaction |
400 | A resource cannot connect to itself. |
ports_required |
400 | A connection needs both sourcePortId and targetPortId. |
port_not_on_resource |
400 | A port you named belongs to a different resource. |
invalid_interaction_id |
400 | The connection id in the path is not a number. |
context_id_required |
400 | legal-targets with scope=context but no contextId. |
invalid_context_id |
400 | contextId is not a number. |
invalid_limit |
400 | legal-targets limit is not a whole number from 1 to 100. limit is optional: leave it out for the complete list. (The resource lists clamp their limit instead.) |
invalid_enroll_into |
400 | GET /v1/resources/search enrollInto is not an id. |
invalid_move_of |
400 | GET /v1/resources/search moveOf is not an id. |
invalid_move_from |
400 | GET /v1/resources/search moveFrom is not an id. |
invalid_resource_update |
400 | A name that is empty after trimming — on PATCH /v1/resources/:id or find-or-create. message says so. |
replace_mismatch |
400 | POST /v1/resource-interactions with replaceInteractionId: that connection does not exist, or does not start at the same resource and port as the new one. message says so. |
exclude_mismatch |
400 | excludeInteractionId is not a connection on that resource and port. |
invalid_workspace |
400 | workspace is not an id. |
workspace_not_in_scope |
400 | workspace names an owner outside your key's reach. |
Not found
| Code | Status | Meaning |
|---|---|---|
port_not_found |
404 / 400 | The port is not on that resource (legal-targets: 404), or a port id you sent does not exist (create: 400). |
interaction_not_found |
404 | No connection has that id. |
resource_not_found |
404 | No resource with that id. |
no_acting_resource |
404 | GET /v1/api-keys/self/view: your key does not act at a resource, so there is nothing to show. |
type_not_found |
404 | No type with that code or id. |
role_not_found |
404 | No role with that id. |
alias_not_found |
404 | No resource bound to that identifier — or it is bound to a resource your key cannot read. An identifier you cannot read does not exist for you; the two cases are deliberately indistinguishable. |
Note the difference between 404 resource_not_found and 403 subject_outside_grant: the first means
nothing exists with that id, the second means something does and it is not yours.
Conflict — the current state refuses
| Code | Status | Meaning | What to do |
|---|---|---|---|
already_a_member |
409 | Already inside that container — on an enrolment, or on a move whose destination already holds it. A move whose two containers are the same also returns this. | Treat as success if you were making it so. |
not_an_active_member |
409 | Not inside that container — on an exit, or on a move from a container it is not in. | Same. |
confirmed_key_locked |
409 | A property in your patch was confirmed by a person. keys lists which. |
Drop those keys and resend. A person must re-confirm before the value can change. |
unique_key_taken |
409 | The resource's type (or a parent type) declares a unique key — the identifier, or one property — and another live resource of the same owner already holds that value. message says so in a sentence naming the owner; holderResourceId, key and value say which resource holds it. Raised by any create, find-or-create, property or identifier change, or unarchive. |
Use a different value, or work with the holder. An archived resource releases its key. |
status_read_only |
409 | The resource sits in a status its lifecycle declares read-only (for example a sent bid). Its own state cannot change: properties, name, identifier, the members it contains, the connections it is the source of, its ports, and un-confirming a property. message says so in a sentence; resourceId and statusId say which resource and which status. Status changes, attestations, archiving, connections pointing at it, and confirming a property stay open. |
Move the resource on through its lifecycle, or make the change on a new revision. |
attestation_not_repeatable |
409 | That event type is declared non-repeatable, and the resource already has one in its current status. message says so in a sentence; resourceId and eventType say which. A status change starts a new count. |
Do not resend. If the act really happened again, the resource must move on first — or the type must be declared repeatable. |
transition_record_undeclared |
409 | The move you asked for records an event when it completes, but that event type is not declared for this resource, or it needs values a move cannot supply. message says which; eventType names it. Nothing moved. |
Do not retry — the lifecycle's declaration needs correcting. Tell whoever administers the account. |
Blocked by the model
| Code | Status | Meaning | What to do |
|---|---|---|---|
port_max_connections |
422 | That port already holds as many connections as it allows. Carries portId and maxConnections. |
Change the existing connection (replaceInteractionId) or remove one first. |
port_inactive |
422 | That port is switched off and takes no connections. | Use another port. |
accepted_type |
422 | That port does not accept resources of the other side's type. | Pick a resource of an accepted type (see the port's effectiveTypes). |
port_direction |
422 | The two ports face the wrong way for this connection. The source port must send (out or bidirectional) and the target port must receive (in or bidirectional). Carries sourcePortId and targetPortId. |
Swap the source and target, or use ports that face each other. legal-targets lists only ports that do. |
compat_authoring_session_only |
403 | Compatibility markings are authored by signed-in people, not keys. | Ask a person to mark it in the web application. |
derive_target_write_blocked |
400 | A property in your patch is computed by a formula. keys names them and where each is computed. |
Remove those keys. They are maintained for you and a manual value would be overwritten. |
attestation_status_gate_unmet |
422 | That event type requires the resource to be at a specific status first. Carries requiredSubjectStatusId and currentStatusId. |
Move the resource to the required status, then attest. |
compat_deny |
422 | The membership would put an item under a setting it is marked incompatible with — a choice made on the new container (or above it) that passes down to what it contains. message names the item and the setting. May carry viaAncestorId (where the setting is made), offendingResourceId (the item that conflicts) and offenderCount; an id is left out when your key cannot read that resource. |
Change the conflicting choice, or put the item somewhere else. |
contents_locked |
422 | The new container (or one above it) locks a setting, and the item — or something inside it — uses a different value. Same optional fields as compat_deny, plus kindTypeId. |
Align the item's setting with the locked one, or put it somewhere else. |
containment_cycle |
422 | The resource you are adding is the container itself, or already contains it — directly or further down. Adding it would put the container inside itself. message names both. |
Pick a different resource, or a different container. |
containment_cap |
422 | The resource's type limits how many containers of the destination's type it can be inside, and it is already inside that many (a move does not count the container it leaves). Carries childId, parentTypeId and maxParents; message names the type. |
Take it out of one of those containers first, or move it instead. |
compat_type_not_connectable |
422 | A compatibility type (compatible_with / incompatible_with) was used for a connection on a port. Compatibility is set between resources, not as a connection. Carries interactionTypeId. |
Use a type from the port's allowedInteractionTypes (GET /v1/resources/:id/ports). |
capture_shape |
422 | A transition's capture is not in the declared shape: an object naming properties, settings or viaPorts, each as a non-empty list or "all" (viaPorts maps a port name to such a list). Raised by the web application's lifecycle authoring; keys never author lifecycles. |
Fix the declaration. |
Advisory reasons (legal-targets only)
These appear inside a legal-targets candidate's reasons, never as a response status.
| Code | Status | Meaning |
|---|---|---|
no_viable_target_port |
— | The candidate passes every rule, but none of its ports can take this connection (direction, accepted type or its own capacity). |
status_read_only |
— | The connection's source — the anchor, or the candidate when the anchor's port receives — sits in a read-only status, so a create would be refused with 409 status_read_only. message names it. |
Refusals passed through from the create/alias doors
| Code | Status | Meaning |
|---|---|---|
find_or_create_refused |
400 / 403 / 409 | find-or-create refused; the reason is in message. 403: the type is not in find_or_create_types. 400: the external system is unknown; or it is resync ("find-or-create is for external systems; 'resync' is the canonical system"); or the type is a built-in system type ("Cannot mint a resource of a system type"). 409: the identifier is already bound to a resource of another type ("… is already bound under … to a resource of another type"), or to a resource your key cannot read; in neither case is that resource named. |
alias_bind_refused |
400 / 403 / 404 / 409 | aliases/bind or identity-claims/redeem refused; reason in message. 403: the external system is not in your key's bind_systems ("key not scoped to bind under '…'") — on redeem this is checked before the code. 409: the identifier is already bound elsewhere; the other resource is not named. A target outside your organization is 403 subject_outside_grant. |
Device credentials — check-in, enrolment and acting for a person
A device credential (auth-and-scopes.md) meets these; an integration key meets only the first, on the device doors.
| Code | Status | Meaning | What to do |
|---|---|---|---|
presence_scopes_required |
403 | This door is for a device credential: check-in needs acts_at_resource_id and actor_source; enrolment redeem needs acts_at_resource_id. |
Use a device credential. |
acts_at_resource_missing |
403 | The resource the device credential acts at no longer exists. | Ask your administrator for a new credential. |
acts_at_outside_owner_grant |
403 | The resource the device credential acts at no longer belongs to the credential's organization. | Ask your administrator for a new credential. |
system_required |
400 | Check-in without system. |
Send the identifier's system by name. |
unknown_system |
400 | No identifier system has that name. | Fix the request. |
alias_code_invalid |
400 | The code is empty once the system's own rule is applied (for example, nothing but punctuation). | Read the identifier again. |
alias_not_enrolled |
404 | No one is linked to that identifier. | Link it first (enrolment redeem). |
no_active_session |
404 | No one is checked in at this device. | Check someone in. |
presence_session_required |
400 | A write by a device credential with actor_source without presenceSessionId. |
Send the id from the check-in. |
presence_session_expired |
401 | The presenceSessionId names a session that ended, expired, or belongs to another device. |
Check the person in again. |
actor_source_presence_session_only |
403 | A device credential with actor_source named an actor (actorResourceId / actorAlias). |
Leave them out; send presenceSessionId. |
enrollment_code_required |
400 | Redeem without an 8-character enrollmentCode. |
Send the code the administrator issued. |
enrollment_not_found |
404 | No pending enrolment has that code — it is wrong, already used, or cancelled. | Ask for a new code. |
enrollment_expired |
410 | The enrolment code has expired. | Ask for a new code. |
enrollment_outside_owner_grant |
403 | The enrolment belongs to another organization than the device credential's. | Redeem it on a device of that organization. |
alias_already_bound |
409 | The identifier is already linked to someone else. | Have it unlinked first. |
Connected apps
An app registered to sign people in (sign-in.md). A connected app is an app registered to sign people in with ReSync: its redirect URIs and the scopes it may use for a
person. An administrator of the owning organization registers and manages it from the organization's page in ReSync;
those administration endpoints are for a signed-in person only and are not part of this API. A key meets only the first
code below. A refusal of the app's allowed_scopes reads as the scope-validation
refusal of key administration.
| Code | Status | Meaning | What to do |
|---|---|---|---|
connected_app_managed |
403 | The resource is a connected app. Its name, redirect URIs and scopes change only through its registration, never through PATCH /v1/resources/:id; and nothing is added to it or removed from it as a member (enrol, exit or move) — its machine keys are managed with the app. |
Ask an administrator of the owning organization to change the registration. |
invalid_client_code |
400 | The app's code is not 2–63 lowercase letters, digits and hyphens, starting with a letter or digit. | Choose another code. |
client_code_taken |
409 | Another connected app already has that code. Codes are never reused. | Choose another code. |
client_code_immutable |
400 | An app keeps its code for life. | Register a new app for a new code. |
invalid_client_name |
400 | The name is empty or longer than 200 characters. | Fix the name. |
invalid_client_kind |
400 | kind must be public. |
Send public. |
invalid_client_owner |
400 | A connected app is registered by an organization, not a person. | Register it under the organization. |
invalid_redirect_uri |
400 | A redirect URI is not an absolute https URL, carries a fragment or credentials, or there are more than 20 — or it names a loopback host (localhost, 127.0.0.1, [::1], *.localhost), which the production issuer refuses whatever the scheme. message names the URI. |
Fix the URI. Redirect URIs are matched exactly. |
connected_app_not_found |
404 | No connected app has that client id. | Check the id. |
connected_app_revoked |
409 | The app is revoked. A revoked app is never changed or reactivated. | Register a new app. |
invalid_client_transition |
409 | The app cannot move to that status from where it is (only active ⇄ suspended, and either → revoked), or it changed meanwhile. | Read the app again. |
key_owner_mismatch |
409 | The key belongs to a different organization than the app. | Use a key of the app's organization. |
invalid_client_status |
400 | The requested status is not one of active, suspended or revoked. |
Choose one of the three. |
key_scope_exceeds_app |
409 | The key may use types the app may not: its operate, attest or read lists reach past the app's allowed types. message names those types. |
Narrow the key, or widen the app's allowed types first. |
key_attached_elsewhere |
409 | The key already belongs to another connected app. A machine key serves one app. | Mint a new key for this app. |
key_not_attached |
409 | That key does not belong to this app (it may have been rotated or revoked already). | Use one of the app's current keys. |
Sign in with ReSync
How an app signs a person in: sign-in.md. Signing a person in to an app is done by the person, in ReSync's web app. A key meets only session_required. The
token and userinfo endpoints of https://auth.limnal.dev answer in the standard OAuth 2.0 / OpenID Connect error shape
instead ({ "error", "error_description" }).
| Code | Status | Meaning | What to do |
|---|---|---|---|
session_required |
403 | Signing in to an app is done by a signed-in person, not a key. | Sign the person in through the app. |
invalid_client |
400 | client_id is not a client id. |
Use the client id your app was given. |
redirect_uri_mismatch |
400 | redirect_uri is not one of the app's registered redirect URIs. They are matched exactly. |
Register the URI, or send one that is registered. |
unsupported_scope |
400 | scope must include openid and may add profile — nothing else. |
Ask for openid (and profile for the name). |
invalid_code_challenge |
400 | The PKCE challenge is missing, is not 43–128 base64url characters, or its method is not S256. |
Send an S256 challenge of your code verifier. |
invalid_state |
400 | state is missing or longer than 512 characters. |
Send a state of 1–512 characters. |
invalid_nonce |
400 | nonce is longer than 255 characters. |
Send a shorter nonce, or none. |
client_not_active |
401 | The access token is for an app that is suspended or revoked. | Ask the app's owner; sign in again once it is active. |
client_not_permitted |
403 | An app signed in for a person cannot use this endpoint (messages, keys, access administration, the person's own settings and others). | Call it as the person in ReSync's web app, or not at all. |
Live updates
Only for those who may hold the stream (signed-in people and device credentials).
| Code | Status | Meaning | What to do |
|---|---|---|---|
live_unavailable |
503 | The live stream is not available right now. | Retry later; meanwhile read the resource. |
live_connection_limit |
429 | Ten streams are already open for this caller. | Close one first. |
live_connection_not_found |
404 | The connectionId is not an open stream of yours (it closed, or the server restarted). |
Open a new stream. |
live_view_limit |
422 | A stream can follow at most 50 views. | Follow fewer. |
invalid_views |
400 | views is not a list of { resourceId, view } with view one of wall, members, inbox, canvas, lifecycle. |
Fix the request. |
Server — not your request
| Code | Status | Meaning | What to do |
|---|---|---|---|
internal_error |
500 | Something went wrong on our side. The body is always { "error": "internal_error", "message": "Something went wrong.", "requestId": "…" } — never the cause. |
Quote requestId (it is also the x-request-id header) when you report it. Retry once with backoff, as Retrying describes. |
Transport
| Code | Status | Meaning | What to do |
|---|---|---|---|
rate_limited |
429 | Your key used its whole budget for the current one-minute window (300 by default; x-ratelimit-limit says your key's own). The window is fixed: it started with your first request. Carries retryAfterSeconds and a retry-after header — the seconds left in the window. |
Wait the stated time, then continue. |
Retrying
Only 429 is safe to retry blindly. Wait for retry-after.
5xx (internal_error, live_unavailable) may be retried once with backoff, but read conventions.md first: writes are not
idempotent, so a PATCH that timed out may already have been applied. Read the resource back before
resending it.
Never retry a 401, 403, 404 or 409 unchanged. Every one of them means the request will be refused
the same way until something changes on your side or your administrator's.