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