Errors and retries
Every error from Director has the same JSON body. Decide what to do from the status code and, where present, the code field.
The error body
{"detail": "invalid or missing API key"}detailis a sentence for people. Show it or log it. Do not match on its text, because wording can change.codeis a stable identifier for programs. It appears on the errors that have one, listed below.field,reason, andvariableappear on some errors and add detail for programs.- Error bodies are JSON whatever
Content-TypeorAcceptyou sent.
Status codes
| Status | Endpoint | Meaning | Retry? |
|---|---|---|---|
| 200 | GET /health | {"status": "ok"} | n/a |
| 200 | POST /v2/decide | A JSON decision response, schema eposyne.director.decision-response/v2 | n/a |
| 400 | POST /v2/decide, Plots and Memory reads, DELETE /v2/worlds/{world_instance_id} | Invalid model selection, or a missing or malformed id | Never. Change the request. |
| 401 | any authenticated endpoint | Missing or invalid credential | Not with the same key. Fix the header or create a new key. (Plugins refresh a short-lived account token and retry once. A key cannot be refreshed.) |
| 403 | any authenticated endpoint | feature_not_in_plan: the account's plan does not include the hosted Director | Never. Upgrade to Pro or higher. |
| 403 | Plots, Memory, and event endpoints | The module is not enabled for the account | Never. Switch the module on. |
| 404 | DELETE /v2/plots/library/{arc_id}, GET /v2/memory/runtime/npcs/{npc_id}, older Plots and Memory routes, every /v1 route | Not found, or contract_retired for a /v1 route | Never unchanged |
| 409 | POST /v2/decide with a stream position, POST /v2/events, PUT /v2/plots/library, DELETE /v2/worlds/{world_instance_id} | The stream position is not reached yet, a timeline has a different parent, or a limit or a busy world, see below | See the cause |
| 406 | every /v2 route | not_acceptable: the Accept header admits no JSON | Never. Fix the header. |
| 413 | POST /v2/decide, any /v2 route with a body | prompt_too_large: the prompt is over 30,000 input tokens; body_too_large: the body is over 262,144 bytes | Never unchanged. Send less. |
| 415 | any /v2 route with a body | unsupported_media_type: the body is not application/json | Never. Fix the request. |
| 422 | POST /v2/decide, POST /v2/events, PUT /v2/policy, PUT /v2/plots/library | The request body was rejected: invalid_json, duplicate_key, nesting_too_deep, schema_violation, value_type, or a module code | Never unchanged. Fix the request. |
| 429 | POST /v2/decide, POST /v2/events, library writes | Rate limit, credits refused, or monthly limit reached; for events and library writes, their own rate limits | Never automatically for a decision. After Retry-After for events and library writes. |
| 502 | POST /v2/decide | The model backend failed | At your discretion, with backoff |
| 503 | POST /v2/decide, and any call with an uncached key | Service or account service unavailable | At your discretion, with backoff |
A call that is refused before the model runs (400, 401, 404, 406, 413, 415, 422, 429, and the 503s for an unreachable account service or billing) costs nothing.
Retry rules that matter
- Never retry a 4xx unchanged. The request or the account has to change first.
- Never retry a 429 automatically. Space out your calls, or fix the cause, then try again from a human decision or a scheduled job.
- Never automatically retry a call that timed out and may have been billed. A timeout on your side does not mean Director did nothing. Check
GET /v2/usage/mebefore you repeat it. - Back off on 502 and 503, for example after 1, 5, and 15 seconds, and give up after a few tries. Meanwhile your game runs its scripted behavior.
Each status in detail
400 Bad Request
The model selection is wrong. The body has "code": "invalid_selection" and field, which is provider or model. See Models and usage.
Or max_tokens is out of range, not a whole number, or repeated: "code": "invalid_max_tokens", "field": "max_tokens", with the accepted min and max. A request with both problems gets the selection error. Nothing runs and nothing is charged.
Or rationale is something other than true or false, empty, or repeated: "code": "invalid_rationale", "field": "rationale". A request that also has a selection or max_tokens problem gets that error first. Nothing runs and nothing is charged.
401 Unauthorized
The body is always the same, so a response tells you nothing about which keys exist:
{"detail": "invalid or missing API key"}Causes: no Authorization header, a scheme other than Bearer, a mistyped or truncated key, a revoked key, or a key from another service. Check that the header is exactly Authorization: Bearer epd_... with one space and no quotes. A revoked key stops working within 60 seconds. If the key is lost, create a new one. Retrying the same key never helps.
403 Forbidden
The credential is valid, but the account's plan does not include the hosted Director. It needs Pro or higher; Free and Starter accounts are refused on every authenticated endpoint. Studio keys and a local Director are not affected. Upgrade the plan, then retry.
{"detail": "'engine' is not included in the Free plan.", "code": "feature_not_in_plan", "feature": "engine", "hint": "Available on Pro and above.", "retryable": false}Or the key is valid, but a module that the endpoint needs is not enabled for the account. Plots endpoints need the Plots module, Memory endpoints need the Memory module, and POST /v2/events needs at least one of the two. POST /v2/decide, the policy, and DELETE /v2/worlds/{world_instance_id} do not need a module. Plots and Memory are off by default. Switch them on under Modules at https://architect.eposyne.com/profile?section=director-keys. A switch reaches API keys within a minute and sign-in tokens within an hour. Do not retry before that.
{"detail": "module 'plots' is not enabled for this tenant"}404 Not Found
Every route under the earlier prefix /v1 answers this body, whatever the method:
{"detail": "director/v1 routes are retired in this runtime; use /v2", "code": "contract_retired"}Change the prefix to /v2 and send the JSON request of The decide request. For the other 404s the body is {"detail": "..."}. On DELETE /v2/plots/library/{arc_id} the library holds no such arc. On GET /v2/memory/runtime/npcs/{npc_id} the timeline was never sent, or Memory holds nothing for that character on it. The older Plots and Memory routes (templates, arcs, trigger, evaluate, entities, runs, context, consolidate, debug) are not available to accounts and answer {"detail": "Not Found"}, as an unknown route does. Do not retry unchanged.
409 Conflict
A decision that names a stream position (world, timeline, after_seq) came before Director held every event up to that position. The body lists what Director holds and what is missing. Resend the missing events, then repeat the decision. Never repeat the same request unchanged in a loop. Plain decisions without a stream position never get this error. On POST /v2/events, a 409 means a timeline already exists with a different parent, or a limit on worlds or timelines was hit. Never retry that one unchanged. Fix the timeline, or delete a world. The codes are in Errors of events, policy, Plots, and Memory.
406 Not Acceptable
{"detail": "Not acceptable: v2 routes answer application/json", "code": "not_acceptable"}The Accept header is present and none of its entries admits application/json (*/* and application/* do). Director answers JSON only. Leave the header out or fix it. No model ran.
413 Payload Too Large
{"detail": "Request body exceeds 262144 bytes", "code": "body_too_large"}A body over 262,144 bytes is refused with "code": "body_too_large" on every /v2 route that takes one. Director also estimates the prompt before it calls a model: a prompt over 30,000 input tokens is refused with "code": "prompt_too_large". Send fewer variables and a shorter ask. See Writing requests.
415 Unsupported Media Type
{"detail": "Unsupported media type: v2 routes take application/json", "code": "unsupported_media_type"}The body came with a Content-Type that is not application/json, or with none, or with a charset other than UTF-8. An XML media type is refused like any other; Director never reads the body to guess a format. Fix the header. No model ran.
422 Unprocessable Entity
The body cannot be processed. The code says why:
code | Meaning | Example body |
|---|---|---|
invalid_json | The body is not a JSON document (an XML document labelled JSON included), or holds a non-finite number. | {"detail": "Cannot parse JSON: malformed document (line 1 column 9)", "code": "invalid_json"} |
duplicate_key | An object repeats a member name, anywhere in the document. | {"detail": "JSON object repeats the member name 'a'", "code": "duplicate_key"} |
nesting_too_deep | The document nests deeper than 32 levels. | {"detail": "JSON nesting exceeds maximum depth (32)", "code": "nesting_too_deep"} |
schema_violation | The JSON is not the route's shape: a missing or unknown member, a wrong schema, a mutable entry with two rules or none, a repeated mutable name. detail names the member. | {"detail": "mutable[0]: exactly one of allowed_values, free_text, range, length is required", "code": "schema_violation"} |
value_type | The JSON type of a variable's current value does not fit its mutable rule. variable names it. | {"detail": "Variable 'guard_morale' is a string but its mutable rule is a numeric range", "code": "value_type", "variable": "guard_morale"} |
A 422 without one of these codes is a partial stream position (one or two of the three parameters), a player_id parameter without a stream position, or an empty or repeated one. On the module endpoints it also means an event batch, a policy, or an arc that does not match its format, or a gap or size limit, see below. Fix the request.
429 Too Many Requests
Three different causes share this status. The detail says which one:
- Rate limit. More than 60 requests a minute for the account. All your keys share this limit.
- Credits refused. The body has
"code": "credits_refused". Your balance is under EUR 1.00, or what is left after the reservations of calls in flight does not cover this call's reservation, or usage credits are off, or this month's credits spend has reached the monthly limit you set. Thedetailtells you which. Add credits or change the setting on the credits page. - Monthly limit. A credits refusal with
"reason": "monthly_limit_reached". The decision that crosses the limit completes; every later one that month is refused until the next UTC month, or until you raise the limit.resets_atinGET /v2/usage/megives the moment.
{"detail": "Your usage credits balance is €0.99, under the €1.00 ...", "code": "credits_refused", "reason": "balance_under_floor"}The reason is balance_under_floor, insufficient_for_hold, credits_off, or monthly_limit_reached. See Billing and limits for how a call reserves its cost.
Two more 429s have rate limits of their own: event_rate_exceeded on POST /v2/events and library_rate_exceeded on library writes. They carry a Retry-After header in seconds and are safe to retry after it. See below.
Show the detail text to the developer or the operator. Do not retry a credits refusal automatically.
502 Bad Gateway
The model backend could not produce an answer: a timeout after 30 seconds, a connection failure, or an unusable reply. A body with "code": "accounting_unavailable" means the provider did not report what the call cost; it is charged its reserved amount. A 502 or 500 after the model has answered is still charged what the call cost; a 502 because the provider failed before answering is not charged. In every 502 case your game should use its scripted behavior. If you retry, do it with backoff, and not after a timeout that may have been billed (see above).
503 Service Unavailable
"code": "account_service_unavailable": Director could not reach the Eposyne account service, so it could not check your key or your credits. No decision was made and nothing was charged. A key that Director checked in the last minute keeps working until that check expires. A key it has not seen recently gets this 503. Retry with backoff."code": "billing_unavailable": Director could not record the call for billing, so it ran nothing and charged nothing. Retry with backoff."code": "model_unavailable": you chose no model and no default is available. Choose a pair fromGET /v2/models.- No code: the decision pipeline is not available on the deployment. Retry later with backoff.
Errors of events, policy, Plots, and Memory
These errors have a code. Match on the code, not on the detail text. A request that one of them refuses stores nothing.
code | Status | Endpoint | Meaning | What to do |
|---|---|---|---|---|
event_rate_exceeded | 429 | POST /v2/events | More than 600 events a minute or 100,000 a day for the account. The whole batch is refused and not counted. | Wait Retry-After seconds, then send the same batch. See Events. |
sequence_gap_too_large | 422 | POST /v2/events | A sequence number is more than 1,000 beyond the acknowledged position of the timeline. | Send the missing events first. See Events. |
player_not_listed | 422 | POST /v2/events | An event names a player_id that is not one of its players. The whole batch is refused. | Add the player to players, or drop player_id. See Events. |
world_limit | 409 | POST /v2/events | The account already has 20 active worlds, and the batch would create a 21st or wake an idle one. | Delete a world you no longer need. See Events. |
timeline_limit | 409 | POST /v2/events | The world already holds 200 timelines. | Delete the world or reuse a timeline. See Events. |
world_busy | 409 | DELETE /v2/worlds/{world_instance_id} | A timeline of the world is being processed and did not finish in about five seconds. | Repeat the same call. It completes. |
library_full | 409 | PUT /v2/plots/library | The library holds 500 arcs and the arc id is new. | Delete an arc, or replace one you hold. See Plots. |
arc_too_large | 422 | PUT /v2/plots/library | The arc document is over 262,144 bytes. | Send a smaller arc. |
library_rate_exceeded | 429 | PUT and DELETE on /v2/plots/library | More than 600 library writes a minute. The write was not made. | Wait Retry-After seconds, then repeat the write. |
policy_invalid | 422 | PUT /v2/policy | The policy does not match its format. errors lists every problem as a path and a message. The stored policy is unchanged. | Fix every listed path. See Policy. |
An arc the library refuses for another reason is a 422 without a code. Its detail starts with arc bundle rejected: and lists every reason. See Plots.
Not errors: deferred and refused judgements
A successful POST /v2/events answers 200 and can carry two extra members. They report model calls of Plots and Memory, not failures of the request.
"deferred": {"count": 70}: events held and acknowledged, but not processed yet, because one request runs at most 50 judgements. They are processed by the next delivery or by a decision with a stream position."refused_judgements": {"count": 2, "reason": "balance_under_floor"}: judgements skipped because your credits could not pay for them. The events are processed as if nothing happened. The reasons are the credits reasons above, plusaccount_service_unavailable,billing_unavailable,prompt_too_large, andmodel_unavailable.
See Events.
Parse defensively
Within /v2 we may add endpoints, optional parameters, response members, headers, and new detail texts. We do not change the meaning of a status code or a code. Ignore members and headers you do not recognize.