# Errors

The error envelope, every code with its status and the fix, and retry guidance.

Every refused request answers with a JSON envelope and a status that says which kind of problem it is. The `message` is written to be shown to a person or fed back to an agent: it names the field or the limit and says what to change.

```json
{
  "error": "plan_ticks",
  "message": "Your Starter plan runs up to 600 rounds per run (asked for 900). Shorten the run or upgrade."
}
```

| Status | Kind | Retry? |
| --- | --- | --- |
| `400` | The body is invalid or over a plan ceiling. The message names what to change. | After changing the body. |
| `401` | No usable token. | With a valid token. |
| `403` | The token’s scopes or the plan do not allow this. | With a different token or plan. |
| `404` | No such scenario or run on this account. | No. |
| `409` | The run is mid-transition. | In a few seconds. |
| `429` | Over the per-minute ceiling. | After the seconds in the `Retry-After` header. |
| `500` | A server fault; nothing you sent was wrong. | Once, then report it. |

## Codes

| Code | Status | Meaning | What to do |
| --- | --- | --- | --- |
| `token_required` | 401 | No token, an unknown one, or a revoked or expired one. | Mint a token under Account > Developer and send it as `Authorization: Bearer dsk_...`. On the MCP server a client that supports sign-in can sign in instead and renews its own token; a connected app’s token works there only, so the REST API answers it with this code. |
| `scope_required` | 403 | The token lacks the scope this call needs; the message names it. | Mint a token with that scope. Scopes cannot be added to an existing token. A connected app holds what was ticked on its approval page: disconnect it under Account > Developer and connect it again, ticking that permission. |
| `plan_api` | 403 | A write (create, update, delete, launch, cancel, publish) on a plan whose tokens are read-only. | Upgrade at /pricing, or do the write from the Studio. |
| `rate_limited` | 429 | Over the token’s per-minute ceiling for the plan, or too many refused requests from one address. | Wait the seconds the `Retry-After` header names, then retry; a refused MCP tool call has no header, so back off a minute. The ceiling is in `GET /me` as `limits.requests_per_minute`. |
| `handover` | 503 | While a deploy moves the world to a new release, the old release holds every write (any non-GET, the MCP endpoints included) and relays it to the new one once that listens. This answer comes only when the new release has not come up within the hold (8 s) or did not answer; then nothing ran. | Send the same request again after the seconds the `Retry-After` header names (3); the new release serves it, with your token, keys and seats intact. The client libraries do this on their own from 0.2.6. |
| `bad_request` | 400 | The body is not a JSON object, is missing fields the message names, or (a run body) carries a field a run does not take; an experiment body’s unknown field answers `bad_experiment`. An unknown field is refused rather than dropped, since the run would play as if it were not there, over REST and the Studio MCP alike. On the REST API a body the server cannot read at all answers this code with its own status: 413 over 1 MB, 415 in a charset or content encoding it does not decode. | Send `content-type: application/json` and an object body under 1 MB; the message names any unknown field. |
| `bad_scenario` | 400 | A scenario field failed validation, or `scenario_id` on a run names nothing you can launch. | The message names the field. Compare against the schema endpoint. |
| `bad_replicates` | 400 | On a run, `replicates` is not the id of an archived Studio match you can re-run: unknown, private, an arena game, or a match with no recipe; or the body also seats a guest (`invited: true`), which a re-run cannot field. On an experiment, it is not the id of a published study (its owner has not published a trial of every arm), or the body also carries a design field (`arms`, `trials`, `seed_mode`, `metric`, `secondary_metrics`, `stopping`, `target_half_width`), which a study’s re-run takes from the study. | Send the match id from a report page or from `/api/matches/<id>/recipe`, with no `invited` seat; for a study, its experiment id alone, with `name`, `max_spend_usd` or `callback_url` at most. Leave it out for an ordinary run or experiment. |
| `profile_required` | 400 | Publishing needs a profile handle: a publish, and a re-run, which is published from creation. | Set a handle in the Studio’s Account view, then publish or launch again. |
| `bad_season_ticks` | 400 | `season_ticks` is outside 10 to 2000 or not an integer. | Send an integer in range, or null for the base default. |
| `bad_frame` | 400 | `frame` is not `monitored` or `neutral`, or was sent on an arena game. | Send one of the two frames on a world run, or leave it out for the canonical prompt. |
| `bad_env` | 400 | A field under `env` is out of range or unknown. An unknown field is refused rather than dropped, since the run would play as if it were not there (the resident fly and the stand-down orders come only from a scenario that has them). | The message names the field; the schema lists every valid one. |
| `too_many_scenarios` | 400 | The plan’s saved-scenario allowance is full. | Delete one, or upgrade. Validate still works. |
| `unknown_scenario` | 404 | No saved scenario with that id on this account. | Ids from other accounts read as unknown by design. |
| `bad_skill` | 400 | A skill `name` is outside 2 to 60 characters or `text` outside 10 to 2000, or a field is not a string. | The message names the field. The bounds are in the skills endpoint as `limits`. |
| `too_many_skills` | 400 | The plan’s saved-skill allowance is full. | Delete one, or upgrade. `limits.saved_skills` in `GET /me`. |
| `unknown_skill` | 404 | No skill with that id: not a built-in and not on this account. On a launch, a roster seat named one. | List skills to find the id. Ids from other accounts read as unknown by design. |
| `bad_roster` | 400 | The roster is empty, malformed, a seat is missing `model` (and is not `invited`), a seat carries more than 4 skills or over 4000 characters of instructions (8000 with skills), an arena seat carries a built-in world skill, a seat carries a field that is not a seat setting (refused rather than dropped, since the seat would play as if it were not there), the roster invites more guest seats than the platform allows, or a seat’s `reasoning` is a level its model’s route refuses (checked at launch with one short call on the seat’s key; the estimate refuses only what an earlier check answered). | One object per seat with at least `model` or `invited: true`; the message names the seat and the field, or the guest-seat cap. For `reasoning`, pick another level or leave it out for the model’s own default. |
| `bad_model` | 400 | A seat’s `model` is not a model id: empty, too long, containing spaces, or a router-style `vendor/model` id on a native vendor that takes its own ids. | Paste the id as the provider names it, not its display name; the message names the seat and echoes what arrived. |
| `bad_name` | 400 | A seat’s `name` is not 2 to 32 letters, digits, spaces, `-` or `_`, or is reserved: the `anchor` and `house-` prefixes belong to the platform’s scripted bots. | The message names the seat; pick another name or leave it out to derive one from the model. |
| `bad_provider` | 400 | A seat names a provider the platform does not route. | Use `openrouter` or a supported native vendor id. |
| `provider_key_required` | 400 | A seat names a native vendor whose key the account does not hold. | Add the key under Account > Provider keys, or route the seat through your router key. |
| `openrouter_key_required` | 400 | A router-routed seat, and the account holds no router key. | Connect or paste a router key under Account > Provider keys. |
| `openrouter_balance_low` | 400 | The router key’s remaining balance cannot cover the estimate. | Top up the key, lower `max_spend_usd`, or shorten the run. |
| `billing_frozen` | 400 | The account is frozen after a payment dispute. | A frozen account cannot be unfrozen from the account pages; once the dispute is settled, write through /contact and Daishi lifts the freeze. |
| `email_unverified` | 400 | A sponsored seat needs a verified email address. | Verify the address from the Studio, or bring your own key. |
| `bad_trials` | 400 | `trials` is not a positive integer, or an experiment asked for fewer than 3 per arm. | Send an integer from 1 (3 on an experiment) up to the plan’s series ceiling. |
| `bad_spend_cap` | 400 | `max_spend_usd` is not a number, or an experiment’s `max_total_spend_usd` is not a number of at least 0.5. | Send a number; a run’s cap is clamped into the plan range, and a study’s cap stands as sent. |
| `bad_loss_tolerance` | 400 | `loss_tolerance` is not a whole number from 0 up to the trials less three. | Send an integer in that range, or leave it out for a tenth of the trials, rounded down. |
| `plan_seats` | 400 | More seats than the plan allows per run. | Shorten the roster or upgrade. `limits.seats_per_run` in `GET /me`. |
| `plan_ticks` | 400 | A season longer than the plan allows. | Lower `season_ticks` or upgrade. Validate warned about this. |
| `plan_series` | 400 | More trials than the plan allows in one batch. | `limits.series_max_trials` in `GET /me`. |
| `plan_quota` | 400 | The month’s agent-turn allowance cannot cover this launch. | Wait for the period to roll, shorten the run, or upgrade. |
| `too_many_runs` | 400 | The plan’s runs-at-once limit is reached: every queued or running run counts, a series counts once, an experiment needs one free per arm, and a world run with guest seats counts only from its launch (while it waits for its guests or for a slot it holds none). | Wait for one to finish, or cancel one. |
| `run_history_full` | 400 | The account holds as many runs as its plan keeps (200 on Free, Starter and Pro, 2,000 on Lab). Every run counts, cancelled and failed ones too. | Runs cannot be deleted. The message gives the count and the plan’s ceiling; a plan with a higher ceiling keeps more, or write through /contact. |
| `seed_not_allowed` | 400 | A pinned `spawn_seed` or `arena.seed` on a multi-trial series, or a seed on a scenario that draws from its seed pool. | Drop the pinned seed, or run a single trial. |
| `unknown_run` | 404 | No run with that id on this account. | List runs to find the id. |
| `unknown_batch` | 404 | No series batch with that id on this account. | The id is `batch.id` on any trial of the batch, or `batch.id` in the launch response. Batches on other accounts read as unknown by design. |
| `unknown_experiment` | 404 | No experiment with that id on this account. | The id is `experiment_id` in the creation response or the list. Experiments on other accounts read as unknown by design. |
| `experiment_trial` | 409 | A cancel on one trial of a paired experiment: dropping a single pair after seeing it would bias the comparison. | Cancel the experiment (`POST /api/v1/experiments/:id/cancel`); the report reads each comparison as of that stop. A trial still ends on its own at the run’s time and spend caps. |
| `experiment_not_running` | 409 | A cancel on an experiment that already finished or was cancelled. | Nothing to do; read its report. The outcome on record stands. |
| `experiment_running` | 409 | A stop reason or a replacement on an experiment that is still queued or running. | Give the reason when you cancel it (`POST /api/v1/experiments/:id/cancel`), or name its replacement once it has ended. |
| `bad_cancel_reason` | 400 | `reason` is missing or not one of `fault`, `design`, `budget`, `results` and `other`; or a reason was sent for an experiment its rule ended, which carries no cancel reason. | Send one word from the list; the message says what each means. The owner chooses it. |
| `cancel_reason_fixed` | 409 | A different reason on an experiment that already says why it was cancelled: a reason on record never changes. | Nothing to do; add what you learned to the study’s note instead. |
| `bad_replaced_by` | 400 | `replaced_by` is not the id of one of your experiments (`exp_...`), or names the experiment itself. | Send the id of the experiment that took its place, or `null` to clear it. |
| `experiment_arms` | 400 | `arms` is not an array of 2 to 4 run bodies. | Send two to four arms, the first the control. |
| `bad_experiment` | 400 | An arm is not an object, carries a field the experiment owns or cannot pair (`trials`, `seed_mode`, `replicates`, `invite`, `invite_minutes`) or one that is not an arm setting at all, seats a guest (a roster entry with `invited: true`), or shares its label with another arm; `aa_check` is true but an arm plays something the control does not, or is not a boolean; `seed_mode` is `pinned` on a scenario that draws its worlds from a seed pool, or `vary` on a scenario with one fixed world; `callback_url` is not an https address on a public host without credentials; or `equivalence_margin` was sent without `replicates`, or is not a positive number of at most 100. | The message names the arm and the field. An arm cannot seat a guest: give that seat a model. An equivalence margin is a re-run’s: send it beside `replicates`, on the metric’s analysis scale. |
| `experiment_scenario` | 400 | The arms name different scenarios. | Every arm must play the same `scenario_id`; vary anything else. |
| `bad_game` | 400 | `game` is not `world`, `chess` or `connect4`. | Send one of the three, or leave it out for a world run. |
| `bad_arena` | 400 | A field under `arena` is out of range, malformed or unknown (an unknown field is refused rather than dropped, since the game would play its default). | The message names the field and its range. |
| `experiment_world_only` | 400 | An arm is an arena game. | An experiment pairs world runs on shared seeds; play arena games as their own series. |
| `experiment_same_arms` | 400 | An arm plays exactly what the control plays: compared on what reaches the match (each seat as validated with its defaults filled in, the environment as the match resolves it, the season and the frame), the two differ in nothing, so the report would compare two copies of one configuration. On a study’s re-run: the study’s arms, as recorded, play one configuration, because they differed only in the owner’s own skills or instructions (which its recipe cannot carry) or in nothing that reaches the match (a spend cap, a name, a setting the server does not read). | Change what the arm is meant to change, or set `aa_check` to true to run identical arms on purpose as a check of the noise. For a re-run, start a variant from the study’s design and write that arm’s text yourself. |
| `experiment_env_seed` | 400 | An arm sets `env.seed` or `env.spawn_seed`, or the saved scenario pins a world seed and `seed_mode` is `vary`. | Drop the seed from the arm; use `seed_mode: "pinned"` to hold one world. |
| `experiment_stopping` | 400 | `stopping` is not `anytime` or `fixed`, `anytime` was asked for with 10 trials or fewer, `target_half_width` is not a positive number on an anytime rule, or `target_half_width` was sent with `fixed`. | Use `fixed` at 10 trials or fewer, or run more trials; drop `target_half_width` on a fixed rule. |
| `bad_metric` | 400 | `metric` is not one of `fitness_index`, `rank`, `survived`, `index_survival`, `index_economy`, `index_social`, `index_adaptation`, `index_competitiveness`, `aggression_per_100_rounds`, `trades_per_100_rounds`, `messages_per_100_rounds`, `map_coverage`, `witness_sensitivity`, `compliance_rate`, `compliance_latency`, `injection_resistance`, `output_tokens_per_turn`, `said_evaluated`, `aggression_between_seats_per_100_rounds`, `trades_between_seats_per_100_rounds`, `betrayals_per_100_rounds`, `ring_share`, or `secondary_metrics` is not a list of up to three of those, distinct and other than the metric. | Send one of those, or leave `metric` out for the Fitness Index; list each secondary outcome once. |
| `bad_design` | 400 | `design` is not a string, holds a NUL character, or is over 100,000 bytes of UTF-8; or an attach sent no `design`. | Send the document as one string of markdown or plain text; the message gives its size when it is too long. |
| `design_final` | 409 | An attach on an experiment whose first trial has started and that already has a design, or on a run set’s setup that already holds runs and a design: what stood when play began cannot be replaced. | Nothing to do; the stored design stands, with its digest and arrival time. Say what changed in the experiment’s or the set’s note. |
| `design_launching` | 409 | An attach while a trial of the experiment is launching: the trial reads the design as it starts, so a change now could leave its record naming another document than the one the page shows. | Wait until the trial has started. A design already attached is then final; a first one is taken under the late-design rule. |
| `design_late` | 409 | A first design attached after the experiment’s first trial started, and the experiment’s name does not carry at least `name_commit_min_hex` hex digits of the document’s SHA-256 (32 on an experiment created since the rule rose, 12 before), so nothing ties the document to the time before the results. | Send the design with the creation, or attach it before the first trial. A design whose digest you put in the name at creation can be attached at any time. |
| `no_design` | 404 | The experiment, or the run set’s setup, has no design document. | Attach one before its first trial starts (`POST /api/v1/experiments/:id/design`), or to the setup (`POST /api/v1/sets/:id/setups/:setup/design`). |
| `no_creation_record` | 404 | The experiment was created before creation records were kept, so it has none. | Nothing to do; experiments created since carry one from the moment they are created. |
| `experiment_effort_roster` | 400 | Output per turn is the metric or a secondary outcome, and an arm fields a different model, or a different number of seats, than the control. | Field the same model in the same seat in every arm and vary something else (a seat’s reasoning setting, the frame, skills, instructions, knobs), or drop output per turn. |
| `experiment_no_probe` | 400 | A probe outcome is the metric or a secondary outcome, and the scenario the arms play lacks the probe it reads: `compliance_rate` or `compliance_latency` without `directives`, or `injection_resistance` with no `anchor:injector` house bot among the anchors of some arm. Every pair would be spent and left out as unmeasured, so the design is refused before it plays. The message names the scenarios that carry the probe. | For the stand-down orders, play `daishi:lab-directives-v1` (a saved scenario cannot add them); for injection resistance, put `injector-practice` among every arm’s house bots (`env.anchors`; `daishi:lab-injection-v1` seats one by default); or drop the probe outcome. |
| `experiment_too_few_seats` | 400 | A reading between seats is the metric or a secondary outcome, and some arm seats too few to read it: `aggression_between_seats_per_100_rounds` and `trades_between_seats_per_100_rounds` need another seat to fight or trade with, so two in every arm, and `ring_share` three, the smallest closed trading ring. Every pair would read zero by construction, so the design is refused before it plays. The message names the arm. | Seat at least two models in every arm (three for `ring_share`; the same model more than once is fine), or drop the outcome. `betrayals_per_100_rounds` counts any recent trading partner, house bots included, and needs no other seat. |
| `plan_experiment_arms` | 403 | The plan’s runs-at-once cannot hold one queue slot per arm. | Use fewer arms, or upgrade. `limits.queued_runs` in `GET /me`. |
| `plan_rerun` | 403 | The plan cannot hold the study’s whole design: its runs at once, trials per batch, seats or rounds per run, or monthly agent turns fall short of what the study declared. The message names each limit and the smallest plan that holds the study. A shortened copy is a different study, so it is never queued as the re-run. | Upgrade to the plan the message names, or build a smaller experiment from the study’s recipe (`GET /api/studies/<id>/recipe`) and queue it under its own name. |
| `unknown_log` | 404 | No log file by that name on the run, or its retention window has passed and it was deleted. | List the run’s logs for the names that are kept. |
| `plan_invites` | 403 | A roster with an `invited` seat on a plan without guest seats. | Upgrade to Pro or Lab at /pricing, or drive every seat yourself. |
| `invite_world` | 400 | An `invited` seat in a world run where runs share one world, so there is no private world to seat a guest in. | Seat the peer’s agent in a chess or Connect Four game instead. |
| `invite_series` | 400 | `trials` above 1 on a run with an `invited` seat: a series would need a link per trial. | Set `trials` to 1, or drop the guest seat from the series. |
| `too_many_open_worlds` | 409 | The plan’s ceiling on runs with guest seats waiting to launch is reached (5 on Pro and 25 on Lab; the refusal names your plan’s limit). Such a run counts until it launches, whether a seat is still open or every seat is claimed and it waits for one of your runs at once; it holds none of your runs at once meanwhile, so this is the one limit on them. | Wait for a waiting run to launch, or cancel one. |
| `no_invite` | 404 | No guest seat with that invite id on the run. | List the run’s invites for the ids; a run without guest seats lists none. |
| `invite_fixed` | 409 | The seat cannot change: the run has launched (or ended), the seat’s deadline has passed, or a regenerate names a seat already forfeited, revoked or expired (a revoke of such a seat answers it as it is). | Nothing to do on this run; withdraw the seat or cancel the run and open a new one. |
| `bad_invite` | 400 | `invite_minutes` (or `minutes` on an extend) is not a whole number from 10 to 4320, `minutes` is missing on an extend, or the extension would pass 72 hours from the run’s creation. | The message says the range, and on an extend how many minutes are left. |
| `never_played` | 400 | A publish on a run, or an add to a run set of a run or a series, that ended without starting a match (cancelled or failed before its first round), so there is no report to list. | Nothing to publish. A run that played and stopped early can be published, marked as stopped early. |
| `run_playing` | 409 | A publish on a run, or an add to a run set of a run or a series with a trial, that is still queued or playing. | Publish or add it once it ends; its report is written then. `GET /api/v1/runs/:id` says when its `status` changes. |
| `study_playing` | 409 | A publish on an experiment or series while one of its trials is still queued or playing; the message says how many. | Publish the study once its last trial ends: `GET /api/v1/experiments/:id` (or `/batches/:id`) says when every trial has. |
| `bad_note` | 400 | `note` is not a string or null, holds a NUL character, or is missing on an experiment’s note, where it is the whole body; on an experiment, also an empty entry (entries are never removed, so there is nothing to clear) or a study whose note already holds 50 entries. | Send the note as a string (on a run, `""` or null clears it; on a publish, leave it out to keep the one the run has). On an experiment, send the entry to add. |
| `note_too_long` | 400 | The note, or an experiment note’s entry, is over 4000 characters, counted after trimming. | Shorten it; the message says how long it is. |
| `unknown_set` | 404 | No run set with that id on this account. | List your sets for the ids (`GET /api/v1/sets`). |
| `unknown_setup` | 404 | The set has no setup with that key; the message lists its keys. | Use one of the keys the set was created with; keys cannot be added later. |
| `bad_set` | 400 | A run set body is malformed: the name or a setup key or label is missing, too long or not a string, a setup is listed twice, the setups are over 12, `show_seat_text` is not a boolean, `labels` is not an object, `runs` or `batches` is not a list of ids, or an add or a remove names nothing. | The message says which field; the set’s name and setup keys are fixed once created. |
| `set_full` | 400 | The add would put more than 60 runs in the set. | Split the runs over two sets, or remove some first. |
| `series_trial` | 409 | An add names one trial of a series by its run id; a series joins a set whole, so its card can say how many trials it holds. | Send the batch id under `batches` instead; the message names it. |
| `in_another_set` | 409 | The run is already in another of your sets; a run is in one set at a time, so a reader finds it in one place. | Remove it from the set the message names first. |
| `guest_seats` | 409 | An add to a run set of a run that seated a guest (a peer’s own agent): the counts sort every agent into roster seats and house bots, and a guest is neither. | Leave runs with guest seats out of sets for now; publish such a run on its own. |
| `set_empty` | 409 | A publish on a set with no runs: there is no card to show. | Add runs or series batches first (`POST /api/v1/sets/:id/members`). |
| `run_launching` | 409 | The run is being handed to the world right now and cannot be cancelled this instant. | Retry the cancel in a few seconds. |
| `internal_error` | 500 | Something failed on the server. Nothing you sent was wrong. | Retry once; if it persists, report the run or scenario id. |

## Over MCP

A tool call that is refused returns a tool error (`isError: true`) whose text is the same envelope, `rate_limited` included. A missing, invalid or expired credential is refused before any tool runs: HTTP `401` with a `WWW-Authenticate` header, and a JSON-RPC error (code `-32001`) whose `data.error` is `token_required`. The per-token flood limit (HTTP 429, with a `Retry-After` header) and server faults are JSON-RPC errors too. A request carries one JSON-RPC message or a batch of up to 10. An empty batch, a larger one, or one whose messages carry two different `api_key` arguments is refused before any tool runs: HTTP `400` with JSON-RPC error `-32600`. Each message of a batch counts on its own against the per-token flood limit.

## Idempotency

Creating a scenario is idempotent on name plus content hash, so retrying a timed-out create is safe. Launching a run is not idempotent: a retried launch queues a second run. Check `GET /api/v1/runs` for a run with your name before retrying a launch whose response you lost.
