Eposyne Director

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

Example: 401
{"detail": "invalid or missing API key"}

Status codes

StatusEndpointMeaningRetry?
200GET /health{"status": "ok"}n/a
200POST /v2/decideA JSON decision response, schema eposyne.director.decision-response/v2n/a
400POST /v2/decide, Plots and Memory reads, DELETE /v2/worlds/{world_instance_id}Invalid model selection, or a missing or malformed idNever. Change the request.
401any authenticated endpointMissing or invalid credentialNot 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.)
403any authenticated endpointfeature_not_in_plan: the account's plan does not include the hosted DirectorNever. Upgrade to Pro or higher.
403Plots, Memory, and event endpointsThe module is not enabled for the accountNever. Switch the module on.
404DELETE /v2/plots/library/{arc_id}, GET /v2/memory/runtime/npcs/{npc_id}, older Plots and Memory routes, every /v1 routeNot found, or contract_retired for a /v1 routeNever unchanged
409POST /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 belowSee the cause
406every /v2 routenot_acceptable: the Accept header admits no JSONNever. Fix the header.
413POST /v2/decide, any /v2 route with a bodyprompt_too_large: the prompt is over 30,000 input tokens; body_too_large: the body is over 262,144 bytesNever unchanged. Send less.
415any /v2 route with a bodyunsupported_media_type: the body is not application/jsonNever. Fix the request.
422POST /v2/decide, POST /v2/events, PUT /v2/policy, PUT /v2/plots/libraryThe request body was rejected: invalid_json, duplicate_key, nesting_too_deep, schema_violation, value_type, or a module codeNever unchanged. Fix the request.
429POST /v2/decide, POST /v2/events, library writesRate limit, credits refused, or monthly limit reached; for events and library writes, their own rate limitsNever automatically for a decision. After Retry-After for events and library writes.
502POST /v2/decideThe model backend failedAt your discretion, with backoff
503POST /v2/decide, and any call with an uncached keyService or account service unavailableAt 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

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:

401
{"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.

403
{"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.

403
{"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:

404 contract_retired
{"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

406
{"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

413
{"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

415
{"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:

codeMeaningExample body
invalid_jsonThe 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_keyAn object repeats a member name, anywhere in the document.{"detail": "JSON object repeats the member name 'a'", "code": "duplicate_key"}
nesting_too_deepThe document nests deeper than 32 levels.{"detail": "JSON nesting exceeds maximum depth (32)", "code": "nesting_too_deep"}
schema_violationThe 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_typeThe 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:

A 429 for a low balance
{"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

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.

codeStatusEndpointMeaningWhat to do
event_rate_exceeded429POST /v2/eventsMore 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_large422POST /v2/eventsA sequence number is more than 1,000 beyond the acknowledged position of the timeline.Send the missing events first. See Events.
player_not_listed422POST /v2/eventsAn 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_limit409POST /v2/eventsThe 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_limit409POST /v2/eventsThe world already holds 200 timelines.Delete the world or reuse a timeline. See Events.
world_busy409DELETE /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_full409PUT /v2/plots/libraryThe library holds 500 arcs and the arc id is new.Delete an arc, or replace one you hold. See Plots.
arc_too_large422PUT /v2/plots/libraryThe arc document is over 262,144 bytes.Send a smaller arc.
library_rate_exceeded429PUT and DELETE on /v2/plots/libraryMore than 600 library writes a minute. The write was not made.Wait Retry-After seconds, then repeat the write.
policy_invalid422PUT /v2/policyThe 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.

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.