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) carry detail specific to that code.
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) 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. |
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 kiosk stations 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 station key (it acts at one place, 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. |
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. |
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 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.