API Error Reference
Every error from the current API (/api/v2/, including /bulk and /changes) answers in one envelope. A response with a non-2xx status has this body:
{
"error": {
"type": "invalid_request",
"code": "invalid_link",
"message": "friends references Character '0123…' which does not exist.",
"param": "friends",
"doc_url": "https://onlyworlds.github.io/api/errors#invalid_link"
}
}| Field | Meaning |
|---|---|
type |
The error family, one of a fixed set: invalid_request, authentication_error, permission_error, not_found, rate_limited, idempotency_error, api_error. It names the family only: one family holds codes with different statuses and different fixes |
code |
The specific, stable identifier. Decide what to do from this, never from type |
message |
Human-readable and safe to log or show. Its wording may change: do not parse it |
param |
The field or query parameter at fault, when there is one; otherwise null |
doc_url |
This page, at the code’s section (#<code>) |
Each code has its own section below, and the summary table lists them all. A fetch of this page gets every section; each code’s section has the id <code>, so doc_url’s #<code> lands on it. The same page as Markdown is at /api/errors.md, where each section is the heading ### <code>. In /bulk, each failed item carries this same envelope: see Bulk Errors. MCP tools report failures as tool errors carrying the same human message, without the envelope fields.
Some errors carry one extra top-level key beside error, named in their row: precondition_failed carries current, the element as it is now. Read the body whole; the envelope is not a closed shape.
Error Codes
Section titled “Error Codes”| Code | Type | Status | What happened |
|---|---|---|---|
invalid_request |
invalid_request |
422 |
The body or query is malformed: an unknown field or query parameter, a bad value, or a wrong-shaped payload; param names the culprit. |
invalid_link |
invalid_request |
400 |
A link field names an element that does not exist in this world (or among a /bulk batch’s surviving items). |
id_conflict |
invalid_request |
409 |
A create supplied an id that already exists, in this world or another (element ids are unique across all worlds). |
resync_required |
invalid_request |
409 |
A guest’s /changes cursor predates a change to what it can see, or is not this caller’s cursor shape: pull again from since=0. |
already_member |
invalid_request |
409 |
The invite names someone who is already a member, or the accepting account already is one. |
removal_in_flight |
invalid_request |
409 |
A removal ticket for this image was issued less than 10 minutes ago and is not yet spent; use it, or retry after Retry-After. |
upload_pending |
invalid_request |
409 |
No image is recorded under this key yet, but an upload ticket in this world is still open and unreported (the upload may have just finished); retry after Retry-After (2 s). The platform cannot tell which key an open ticket will get, so any key answers this while the caller holds one, until it expires; then not_found. |
pin_required |
invalid_request |
409 |
A member without an account PIN tried to accept an invite or mint a write key. |
precondition_failed |
invalid_request |
412 |
The request carried If-Match with a change_seq, and the element has moved since; the reply’s top-level current is the element as it is now. Nothing was written and nothing is merged: re-read current, then re-send or ask the user. |
apply_failed |
invalid_request |
422 |
A /bulk item passed validation but its write failed a database constraint (most often an id that already exists in another world). |
invalid_credentials |
authentication_error |
401 |
The API key or PIN is missing, unrecognised or wrong. |
key_revoked |
authentication_error |
401 |
The API key was recognised but has been revoked. |
permission_error |
permission_error |
403 |
The credential is valid but lacks the scope for this route (e.g. a read key on a write route). |
not_author |
permission_error |
403 |
A contributor or guest key changed, replaced, relinked or deleted an element someone else created. |
owner_only |
permission_error |
403 |
A member key, even a co-builder’s, tried to change the world’s own settings. |
guest_not_supported |
permission_error |
403 |
A guest key called a route guests cannot use yet. |
storage_full |
permission_error |
403 |
The account the upload counts against has used all of its image storage. |
not_found |
not_found |
404 |
The element or route does not exist in this world. |
rate_limited |
rate_limited |
429 |
Too many failed PIN attempts; retry after the Retry-After header’s seconds. |
quota_exceeded |
rate_limited |
429 |
The world has used its image upload tickets for the day; retry after Retry-After. |
idempotency_error |
idempotency_error |
409 |
An Idempotency-Key was reused with a different request body. |
api_error |
api_error |
500 |
An unexpected server-side error; the envelope is kept even here. |
server_busy |
api_error |
503 |
Every request slot stayed full for 10 seconds; retry after Retry-After. |
payload_too_large |
api_error |
413 |
The request body is over 2.5 MB; split a large /bulk into several calls. |
media_unavailable |
api_error |
503 |
Image upload is not configured on the server right now. |
invalid_request
Section titled “invalid_request”Type invalid_request · Status 422
The request body or query is malformed: an unknown field, a server-managed field in a write, an unknown query parameter, a value that fails validation, text over its length limit, extension fields over the size cap, a malformed id, or a wrong-shaped payload.
- Common cause: a misspelled field name (
freinds); a_idsor_idsuffix carried over from the Classic API (the current API uses bare link names); an unknown query parameter, such as a mistyped filter (nmae__icontains) or?ordering=; sending a read body back withtype,created_at,updated_atorchange_seqstill in it; a bulk request whoseitemsis not an array. - How to fix: read
param: it names the field or parameter at fault. Correct the spelling, drop the suffix or the server field, or remove the parameter. The list filters arename,name__icontains,supertypeandsubtype, pluscharacterson the six categories with that link; the other accepted parameters arelimit,cursor,expandandfields. Any other query parameter is rejected, never ignored.
Unknown fields are a hard error, not a silent drop, so a mistyped field name fails loudly instead of vanishing. Fields under the extension namespaces atlas_*, shadow_* and x_* are the exception: they are stored as written.
invalid_link
Section titled “invalid_link”Type invalid_request · Status 400
A link field names a UUID that is not an element of the linked category in this world.
- Common cause: linking to an element that was never created, was deleted, or whose UUID was mistyped. An id of an element of another category (a Location’s id in
friends, which holds Characters). In/bulk, linking to a sibling item that itself failed. For a guest key, linking to an element the guest cannot see. - How to fix: check that the referenced element exists and is of the category the field names (read it first, or include it in the same
/bulkbatch). Links in a batch are checked against the world plus the batch’s surviving items, in any order, but an item that fails cannot be linked to.paramnames the link field.
A single write that fails with invalid_link writes nothing, so after fixing the link you can retry it with the same element id. A dangling link is an explicit, named error. The Classic API’s old behaviour of silently dropping it does not apply here.
id_conflict
Section titled “id_conflict”Type invalid_request · Status 409
A POST supplied an id that already exists, or a PUT named an id held by an element in another world. Element ids are unique across all worlds, so the collision may be with another world. The message says which case it is.
- Common cause: retrying a create with a client-minted UUID that already landed, or reusing ids copied from another world.
- How to fix: if the id exists in this world, use
PUT /api/v2/{type}/{id}, the upsert:POSTonly creates. If it exists in another world, mint a new id. To retry a request that may have completed, send anIdempotency-Keyheader instead of posting again.
precondition_failed
Section titled “precondition_failed”Type invalid_request · Status 412
A PATCH or a link operation carried If-Match with a change_seq, and the element has changed since. Nothing was written and nothing is merged. The reply carries, beside the error, a top-level current: the element as it is now, as this caller sees it.
- Common cause: an edit made offline or from a stale copy, while another client (Atlas, a plugin, an agent) changed the same element.
- How to fix: read
current. If the fields you changed are not the ones that moved, re-send yourPATCHwithcurrent.change_seqinIf-Match. If the same field moved on both sides, ask the user which version to keep. See Only if unchanged.
resync_required
Section titled “resync_required”Type invalid_request · Status 409
A guest’s /changes cursor is from before a change to what the guest can see, or the cursor is not this caller’s shape (a guest’s has three parts, everyone else’s two).
- How to fix: pull the feed again from
since=0and replace the local copy. See Guests’ Cursors.
already_member
Section titled “already_member”Type invalid_request · Status 409
An invite names someone who is already a member of the world, or an invite is accepted by an account that is already a member.
- How to fix: nothing to do; the membership exists. Check the roster with
GET /api/v2/members.
pin_required
Section titled “pin_required”Type invalid_request · Status 409
A member without an account PIN tried to accept an invite or mint a write key. Member keys write with the member’s own account PIN.
- How to fix: set a PIN in your account settings, then retry.
removal_in_flight
Section titled “removal_in_flight”Type invalid_request · Status 409
POST /api/v2/media/remove-ticket: a removal ticket for this image was issued less than 10 minutes ago and hasn’t been used yet.
- How to fix: use the ticket you already have, or retry after the
Retry-Afterheader’s seconds.
upload_pending
Section titled “upload_pending”Type invalid_request · Status 409
POST /api/v2/media/remove-ticket: no image is recorded under this key yet, but an upload ticket in this world is still open and unreported. The upload may have just finished. The platform can’t tell which key an open ticket will get, so while the caller holds an open ticket (the owner key: any ticket in the world; anyone else: their own), any key answers this until that ticket expires, 10 minutes after it was issued. After that a key with no image answers not_found.
- How to fix: retry after the
Retry-Afterheader’s seconds (2).
invalid_credentials
Section titled “invalid_credentials”Type authentication_error · Status 401
The API key or PIN is missing, unknown or wrong.
- Common cause: no
API-Keyheader, a mistyped key, or a missing or wrongAPI-Pinon a write. Only a legacy 10-digit key also needs the PIN to read a private world; prefixed keys (ow_w_,ow_r_) read without it. A deleted world’s keys are deleted with it, so they also answerinvalid_credentials. - How to fix: check the
API-KeyandAPI-Pinheaders (exact names) and that the key belongs to the world you mean. Themessagetells the two apart:No valid API-Key.for the key,Incorrect PIN.for the PIN. A key that reads with200but writes with401has a wrong PIN, since reads with a prefixed key never check it. Keys are minted in the account portal. The PIN is a 4-digit number (1000 to 9999) set on your account in account settings, and it guards writes to every world you own; a member writes with their own account PIN, and an agent seat sends its seat secret (ow_s_…) asAPI-Pin. See Keys and PINs.
key_revoked
Section titled “key_revoked”Type authentication_error · Status 401
The API key was recognized but has been revoked.
- Common cause: a key that was revoked in the account portal is still used by an old client or script.
- How to fix: mint a new key in the account portal and update the client. Revocation is permanent for that key.
permission_error
Section titled “permission_error”Type permission_error · Status 403
The credential is valid but lacks the scope for this route.
- Common cause: a read key (
ow_r_…) on a write route: create, update, delete, link operations, bulk, or an image ticket. - How to fix: use a write key (
ow_w_…, or a legacy key) for writes. A403means the key is genuine and cannot do this; a401invalid_credentialsmeans the key itself is not accepted.
not_author
Section titled “not_author”Type permission_error · Status 403
A contributor or guest key changed, replaced, relinked or deleted an element someone else created. In /bulk it is reported per item.
- How to fix: these roles change only their own elements (
created_bynames the creator). Ask the owner for the co-builder role, or leave the element alone. See Roles.
owner_only
Section titled “owner_only”Type permission_error · Status 403
A member key, even a co-builder’s, tried to change the world’s own fields (PATCH /api/v2/world).
- How to fix: only the owner’s key can do this.
guest_not_supported
Section titled “guest_not_supported”Type permission_error · Status 403
A guest key called a route guests cannot use (world sharing, token status).
- How to fix: use a key with a role other than guest, or skip the route.
storage_full
Section titled “storage_full”Type permission_error · Status 403
POST /api/v2/media/ticket: the account the upload counts against has used all of its image storage. The message names the cap.
- How to fix: no ticket is issued. Use an image hosted elsewhere (
image_urltakes any URL), or contact info@onlyworlds.com about the cap.
not_found
Section titled “not_found”Type not_found · Status 404
The requested element or route does not exist.
- Common cause: a read,
PATCHor link operation on an element UUID that is not in this world, or a mistyped path: an address under/api/that no route matches answers this envelope too, never a web page. For a guest key, an element the guest cannot see. An agent join code that is unknown, used, revoked or expired also answersnot_found, the same answer for every case. - How to fix: check that the UUID exists in this world and that the path is right.
DELETEis idempotent: deleting an element that is already gone returns204, not404.
rate_limited
Section titled “rate_limited”Type rate_limited · Status 429
Too many failed authentication attempts, such as repeated wrong PINs.
- Common cause: a script retrying with a wrong PIN in a tight loop.
- How to fix: wait the number of seconds in the
Retry-Afterheader. Repeated failures escalate to a temporary lockout, so fix the credential before retrying.
quota_exceeded
Section titled “quota_exceeded”Type rate_limited · Status 429
POST /api/v2/media/ticket: the world has used its 200 image upload tickets for the day.
- How to fix: retry after the
Retry-Afterheader’s seconds.
idempotency_error
Section titled “idempotency_error”Type idempotency_error · Status 409
An Idempotency-Key header was reused with a different request body.
- Common cause: reusing one idempotency key across two different requests.
- How to fix: one key names exactly one request: use a new key (a new UUID) for each distinct write. Replaying the identical body with the same key is fine: it returns the original stored response, with an
Idempotent-Replay: trueheader, and does not write twice. See Idempotency.
api_error
Section titled “api_error”Type api_error · Status 500
An unexpected server-side error. The envelope holds even here: the API never answers with a bare HTML error page.
- Common cause: a bug or a passing infrastructure fault on the server, not your request’s shape.
- How to fix: retry after a short delay. If it persists, report it to info@onlyworlds.com with the time and the request. Server errors are logged on our side.
server_busy
Section titled “server_busy”Type api_error · Status 503
Every request slot on the server stayed full for 10 seconds.
- How to fix: retry after the
Retry-Afterheader’s seconds.
payload_too_large
Section titled “payload_too_large”Type api_error · Status 413
The request body is over 2.5 MB. The server refuses it without writing anything.
- How to fix: send less per request: split a large
/bulkbatch into several, and upload images through image upload, never inside a JSON body.
media_unavailable
Section titled “media_unavailable”Type api_error · Status 503
POST /api/v2/media/ticket: image upload is not configured on the server right now. There is no Retry-After.
- How to fix: retry later, or use an image hosted elsewhere. If it persists, report it to info@onlyworlds.com.
Bulk Errors
Section titled “Bulk Errors”POST /api/v2/bulk answers HTTP 200 once the batch is read. (A malformed request, such as bad JSON, items not an array or over 1000 items, or an authentication failure answers with its own status and the envelope.) Success and failure are reported per item, in request order, under a top-level errors flag:
{
"errors": true,
"items": [
{ "status": 201, "id": "0695…", "created_at": "…", "updated_at": "…" },
{ "status": 400, "id": "0698…",
"error": {
"type": "invalid_request",
"code": "invalid_link",
"message": "location references Location '…' which does not exist in this world or among the batch's surviving items.",
"param": "location",
"doc_url": "https://onlyworlds.github.io/api/errors#invalid_link"
} }
]
}errorsistrueif any item failed,falseif all succeeded.- Each item’s
erroruses the same envelope as above. The codes seen per item areinvalid_request(unknown field, wrong shape),invalid_link(a reference to a missing element),not_author(403, a contributor or guest touching someone else’s element) andapply_failed. - Each item’s
statusis what a single write would have returned:201created,200replaced, or400,403or422for the errors above. - Partial success is the default: one bad item does not stop the batch. Send
"atomic": truefor all or nothing. See Bulk.
apply_failed
Section titled “apply_failed”Type invalid_request · Status 422, per bulk item
The item passed validation but the write itself failed a database constraint. The most common cause is an id that already exists: element ids are unique across all worlds, so a copied element can collide with its original in another world. The message names the failing element’s id.
- How to fix: give the element a new UUID, or remove the other copy.
Upload Host Errors
Section titled “Upload Host Errors”The image upload to upload.onlyworlds.com is not part of the API and answers errors in its own shape, {"error": "<code>"}. Its open paths are POST https://upload.onlyworlds.com/v1/upload and POST /v1/remove; the host’s root and every other path redirect to a login on purpose, and are not for browsers. A successful upload answers 201 with {url, key, bytes, type, etag}.
| Status | Code | What to do |
|---|---|---|
401 |
ticket_required |
Send the ticket as Authorization: Bearer <ticket>. |
401 |
ticket_invalid |
Send the ticket exactly as POST /api/v2/media/ticket returned it. |
401 |
ticket_expired |
Tickets last 10 minutes: get a new one. |
401 |
ticket_used |
One ticket, one upload: get a new one for the next image. |
400 |
bad_key |
The X-Key is not allowed: it must start with the ticket’s prefix, use only a-z 0-9 . _ - and single slashes, stay within 200 characters, and carry an extension that matches the file’s bytes. The response’s reason field names the cause. |
400 |
empty_body |
The request had no image bytes. |
411 |
content_length_required |
Send a Content-Length header. |
405 |
method_not_allowed |
Only POST uploads. |
409 |
exists |
That X-Key is taken and objects are never overwritten: choose another, or omit X-Key. |
413 |
too_large |
The image is over the ticket’s max_bytes. |
415 |
unsupported_type |
Send webp, png, jpeg or avif (the type is read from the bytes; no SVG). |
502 |
write_failed |
Nothing was stored and the ticket is still unspent: retry with the same ticket. |
403 |
not_removable |
The image was not uploaded into the removal ticket’s world with a ticket, so it can’t be removed this way. |
502 |
remove_failed |
Nothing was removed and the removal ticket is still unspent: retry with the same ticket. |
503 |
ticket_lane_unconfigured |
Uploads are not configured on the host right now: retry later. |
