TauOpt REST API reference#

The hosted TauOpt service is a separate, closed repository; this page is rendered from its OpenAPI document (info.title tauopt, info.version 0.1.0, OpenAPI 3.1.0) dumped on 2026-10-31. The Python client in this repository wraps the experiment, suggestion, and observation routes below through the ask loop’s methods (Python API), and GET /v1/experiments, GET /health, and GET /v1/auth/check through Client.experiments(), Client.health(), and Client.auth_check(); the experiment summary, the CSV export of observations, GET /v1/me and GET /v1/usage/me are REST-only (the web app’s reads), and the session and token-management routes are the web app’s alone. The MCP server exposes twelve of the routes as tools (MCP tools). Bodies are JSON. Ids are integers in paths and strings in response bodies. Response objects are the wire models of the Python API (experiment, suggestion, observation). The hosted plane names the service build it runs in GET /health; this document describes the build it was dumped from.

Routes#

GET /health#

Health. Requires no authentication.

Responses:

  • 200 – Successful Response: {"status": "ok", "build": {...}} after a database round-trip – build names the service commit (service_sha), the engine (engine_version, engine_sha), the dependency fingerprint (deps_fingerprint) and the lock hash (uv_lock_blake2b) the image was made from; outside the pre-authentication gate and the /v1 bucket

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1) – rate_limited – beta lane only: /health has its own token bucket (5 requests per second, burst 10, the uptime checks’ budget, never shared with the /v1 bucket) and it is empty; Retry-After 1

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/auth/check#

Auth Check. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Responses:

  • 200 – The caller’s user id; the queue state on the headers: {"user_id": <integer>}; the request is not counted as usage. Since the queue (2026-10-24) the caller’s queue state rides the RESPONSE HEADERS: X-Tauopt-Inflight (asks admitted to the master queue or running on a worker, across every instance), X-Tauopt-Inflight-Limit (the per-user bound the QUEUE applies: the organization’s override, else the service default of 8; the plan’s inflight_per_user column is informational since deploy 2 retired the inline lane) and X-Tauopt-Queued (asks waiting in the caller’s own queue) – the numbers a client sizes its own parallelism by

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/auth/session#

Bootstrap. Authentication: a fresh Identity Platform ID token in Authorization: Bearer (the web app’s sign-in; a tau_ token answers 401).

Responses:

  • 201 – Successful Response: {"user_id": <integer>, "email": "<the verified address>", "expires_at": "<ISO 8601>"} and the session cookie in Set-Cookie (__Host-tauopt_session; Secure; HttpOnly; SameSite=Lax; seven days, re-issued while the session is in use). The one credential is a fresh Identity Platform ID token in Authorization: Bearer, never a tau_ token: the web app’s sign-in calls this route, a script does not

  • 401 – unauthorized (the ID token did not verify, is stale, or names no admitted account; the body never says which)

  • 403 – account_pending | account_disabled (a verified identity’s own account is not active) | origin_refused (no allowed Origin) – account_pending / account_disabled – a verified identity whose account is not active (the cookie is cleared); origin_refused – no allowed Origin on the request

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

DELETE /v1/auth/session#

Logout. Authentication: the web app’s session cookie (a tau_ token answers 401).

Responses:

  • 204 – Successful Response: 204 with the cookie cleared (Max-Age=0), whether or not the presented cookie was live; under the session cookie only

  • 401 – unauthorized (a token or bearer on a session route)

  • 403 – origin_refused (no allowed Origin beside the session cookie) – origin_refused – no allowed Origin beside the session cookie

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/experiments#

List Experiments. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Query parameters:

Name

Type

Required

Default

Constraints

limit

integer

no

50

>= 0; <= 2147483647

offset

integer

no

0

>= 0; <= 2147483647

q

string | null

no

null

length 1..255

sort

"created" | "updated" | "name" | "observation_count"

no

"created"

–

dir

"asc" | "desc"

no

"desc"

–

include_deleted

boolean

no

false

–

Responses:

  • 200 – Successful Response: {"count": <total under the filters>, "data": [experiment, ...]} – by default the caller’s ACTIVE experiments, newest first (sort=created, dir=desc), each WITH progress; q narrows by name (a case-insensitive substring), sort is created, updated, name or observation_count, dir is asc or desc, include_deleted=true adds the archived ones; limit is clamped to 1..200 and a negative offset is a 400

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/experiments#

Create Experiment. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Request body: ExperimentIn (required).

Responses:

  • 201 – Successful Response: the experiment object with progress; its suggestion_ttl_s echoes the request (null when the service default of 86400 s applies; 0 never expires)

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure naming the field; provide exactly one of metric | metrics; metric strategy/threshold require metrics mode: use metrics=[...]; an engine space or metric rule; num_solutions is single-objective only; constraint region too small: 0/4096 sobol probe points feasible; a bare string metric, refused with the message the Python client answers before any request leaves (Troubleshooting); a NUL byte, a lone surrogate, or a non-finite number anywhere in the body, one message per class (NUL bytes are not allowed; strings must be valid UTF-8 (lone surrogates are not allowed); non-finite numbers (NaN, Infinity) are not allowed)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – experiment_quota_exceeded / quota_unverifiable (the active-experiment quota) | rate_limited / credential_rate_limited (the beta lane) – experiment_quota_exceeded – the active-experiment cap is reached; no Retry-After (archive an experiment first) – quota_unverifiable – the quota store could not be read; Retry-After 30

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/experiments/{experiment_id}#

Get Experiment. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 200 – Successful Response: the experiment object with progress, including expected_suggest_s (the median of the last five ask durations in seconds; null until one exists); archived experiments are returned with state deleted

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

PATCH /v1/experiments/{experiment_id}#

Patch Experiment. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Request body: ExperimentPatch (required).

Responses:

  • 200 – Successful Response: the updated experiment object with progress; suggestion_ttl_s sent as an explicit null reverts the experiment to the service default, an absent field leaves it unchanged

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure naming the field; metric_thresholds requires metrics mode; unknown metrics [...]; an engine metric rule on the new thresholds; a NUL byte, a lone surrogate, or a non-finite number anywhere in the body, one message per class (NUL bytes are not allowed; strings must be valid UTF-8 (lone surrogates are not allowed); non-finite numbers (NaN, Infinity) are not allowed)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, another user’s experiment, or an archived experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – experiment_busy (another request holds the experiment; Retry-After 1) | connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1) – experiment_busy – another request holds the experiment’s lock and the 3 s bounded wait expired; Retry-After 1 (the ownership read precedes the lock, so a 404 never waits)

DELETE /v1/experiments/{experiment_id}#

Archive Experiment. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 204 – Successful Response: no body; the experiment stays readable by id with state deleted; there is no unarchive

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, another user’s experiment, or an experiment that is already archived

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – experiment_busy (another request holds the experiment; Retry-After 1) | connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1) – experiment_busy – another request holds the experiment’s lock and the 3 s bounded wait expired; Retry-After 1 (the ownership read precedes the lock, so a 404 never waits)

GET /v1/experiments/{experiment_id}/best_assignments#

Best Assignments. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 200 – Successful Response: {"count": n, "data": [...]} – the single best observation, the diverse top-k when num_solutions > 1, or the threshold-filtered Pareto frontier when multimetric; {"count": 0, "data": []} when nothing has succeeded; archived experiments included

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/experiments/{experiment_id}/observations#

List Observations. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Query parameters:

Name

Type

Required

Default

Constraints

limit

integer | null

no

null

>= 0; <= 2147483647

offset

integer

no

0

>= 0; <= 2147483647

sort

string

no

"created"

–

dir

"asc" | "desc"

no

"asc"

–

Responses:

  • 200 – Successful Response: {"count": n, "data": [observation, ...]} – by default in id order (report order: sort=created, dir=asc); sort is created, value (the first OPTIMIZED metric: the v0 scalar, or in metrics mode the entry metric_name mirrors), value-<metric> or a parameter name, dir is asc or desc, with the id as the secondary key and an unsortable row last; anything else is a 400; count is the total; limit absent lists every row, present it is clamped to 1..200, and a negative offset is a 400; archived experiments included

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/experiments/{experiment_id}/observations#

Post Observation. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Request body: ObservationIn (required).

Responses:

  • 201 – Successful Response: the observation object; observing a suggestion closes it and frees its bandwidth slot

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure naming the field; provide exactly one of suggestion_id | assignments; provide value(s) XOR failed=true; an engine rule on the assignments or metric values; an observation past 32768 bytes serialized (observation body exceeds 32768 bytes serialized); a NUL byte, a lone surrogate, or a non-finite number anywhere in the body, one message per class (NUL bytes are not allowed; strings must be valid UTF-8 (lone surrogates are not allowed); non-finite numbers (NaN, Infinity) are not allowed)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown experiment or suggestion id, another user’s experiment, or an archived experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 409 – conflict (the suggestion already has an observation) | suggestion_expired (details.assignments to re-post) | observation_cap_reached (details.cap / count) – conflict – the suggestion already has an observation – suggestion_expired – the suggestion was closed by the TTL expiry and its slot reissued; details carries suggestion_id, expired_at, created_at, and the assignments for an unsolicited re-post (send assignments instead of suggestion_id); an observe that loses the row-lock race to the expiry gets this answer rather than a silent 201, while one that wins the row lock closes the suggestion first and answers 201 – observation_cap_reached – the experiment already holds its organization’s plan’s max_observations_per_experiment (4000 on the beta plan, 2000 on the free plan; details.cap, details.count), checked before the suggestion branch so an open suggestion stays open: archive the experiment and start a new one, dismiss what you cannot report, or, with repository access or at the public release, run the local driver

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/experiments/{experiment_id}/observations.csv#

Export Observations. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 200 – The complete observation history as CSV (an attachment): the experiment’s observations as a UTF-8 CSV stream (a BOM first), REST-only: the Python client has no method for it

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/experiments/{experiment_id}/suggestions#

List Suggestions. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Query parameters:

Name

Type

Required

Default

Constraints

state

"open" | "closed" | null

no

null

–

tag

string | null

no

null

length 1..128

limit

integer | null

no

null

>= 0; <= 2147483647

offset

integer

no

0

>= 0; <= 2147483647

Responses:

  • 200 – Successful Response: {"count": n, "data": [suggestion, ...]} in id order; archived experiments included; expired_at is set on a suggestion the TTL expiry closed and null otherwise; tag is the suggestion’s worker label or null, and ?tag= narrows the list to the exact match, beside ?state= – count is the total under both

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/experiments/{experiment_id}/suggestions#

Post Suggestion. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Request body: SuggestionIn (optional; an empty body or null is accepted).

Responses:

  • 201 – The open suggestion; the queue state on the headers: the suggestion object: state open, the provenance receipt, intent when the suggestion is an authorized duplicate, and tag, the request’s worker label (null when the ask carried none). Before the bandwidth check, every open suggestion of the experiment older than its suggestion_ttl_s (the service default of 86400 s when null; 0 disables) is closed with expired_at set and its slot freed – lazy expiry, on this route only, so an orphan past its TTL still lists as open until the experiment’s next ask. Since the queue (2026-10-24) the ask runs on a worker pool: the handler enqueues and waits within the ask’s bound, and the admission state rides the response headers of every answer served through the queue – the 201, or a typed refusal the ask ends in after the enqueue – and of a too_many_queued: X-Tauopt-Admission (direct | queued | refused_queue | refused_parked | refused_share), X-Tauopt-Queue-Ms (arrival to lease), X-Tauopt-Inflight, X-Tauopt-Inflight-Limit and X-Tauopt-Queued; the door refusals (queue_full, too_many_parked) carry only X-Tauopt-Admission and X-Tauopt-Queue-Ms of the five (with Retry-After 1), a refusal from the prefix behind the door (a quota 429, the 409 idempotency_key_mismatch, the prefix-stage 503) carries none of the five, and the idempotency joiner’s 429 Retry-After alone. An Idempotency-Key request header makes a retry JOIN the original ask’s outcome (the 201 body or the typed error as first served) instead of starting a second ask; the key is scoped to the caller and the experiment

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure naming the field; config: <reason> when config fails OptimizerConfig validation; intents: <reason> for a malformed intent; tag: <reason> for a worker label outside 1 to 128 printable characters; an ask-argument rule (for example intents without dedup); a NUL byte, a lone surrogate, or a non-finite number anywhere in the body, one message per class (NUL bytes are not allowed; strings must be valid UTF-8 (lone surrogates are not allowed); non-finite numbers (NaN, Infinity) are not allowed)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, another user’s experiment, or an archived experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 409 – space_exhausted (details.terminal: proven completion) / unique_candidate_unavailable and the other draftless statuses (details.dedup) | idempotency_key_mismatch (the key was used against another experiment) – idempotency_key_mismatch – the Idempotency-Key was used against another experiment; details.experiment_id names it – space_exhausted – proven exhaustion; details.dedup carries the proof and details.terminal is true; the same answer repeats until the experiment state changes – unique_candidate_unavailable – no unique candidate and no proof; details.dedup carries the record; the same answer repeats until the experiment state changes

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – bandwidth_full (Retry-After) | quota_exceeded / quota_unverifiable (the suggest quota) | rate_limited / credential_rate_limited (the beta lane) | too_many_queued (the per-user queue cap: Retry-After 1; details.inflight / inflight_limit / queued / queued_limit) | suggest_rate_limited (the per-user suggest rate on either path, ~10 a second with a burst of 20: Retry-After 1; details.rate_per_s / burst) | too_many_inflight_asks (a retry past the per-user joiner cap on one Idempotency-Key, queue_join_waiters_per_user: Retry-After 1; details.inflight / inflight_limit / queued / queued_limit / joined) | too_many_parked (the per-user CEILING on an instance’s parked permits on the lanes with the credential door – the share beside other holders, the allowance alone – a capacity event, never a flooded mark: Retry-After 1; admission refused_share; details.parked / parked_share / max_parked) – too_many_inflight_asks – a retry under the same Idempotency-Key already waits on one of the caller’s in-flight asks past the per-user waiter cap; Retry-After 1; details carries inflight, inflight_limit, queued, queued_limit and joined – too_many_queued – the caller’s own queue holds queued_limit asks waiting behind its in-flight bound (the organization’s override, else the service default of 8; the plan’s inflight_per_user column is informational since deploy 2 retired the inline lane); Retry-After 1; X-Tauopt-Admission: refused_queue – too_many_parked – the caller holds its share of this instance’s suggest permits beside the other tenants, or its allowance alone; Retry-After 1; X-Tauopt-Admission: refused_share; details carries parked, parked_share and max_parked – bandwidth_full – open suggestions already equal parallel_bandwidth; Retry-After 1 – quota_exceeded – the rolling 24-hour suggest quota; Retry-After is the time until the oldest counted suggestion leaves the window – quota_unverifiable – the quota store could not be read; Retry-After 30

  • 500 – all_rungs_failed / integrity_violation (the engine’s terminal failures: a constant message per type; details carry the typed evidence – each attempt’s rung, stage and exception class, intended, check – never the exception text, which stays in the service log under the response’s X-Request-Id) | internal_error (an engine-side ValueError, details.exception_type; or the constant crash envelope) – all_rungs_failed or integrity_violation – the engine’s AskFailed: every rung failed (details.intended, details.attempts – each attempt’s rung, stage and exception_type) or a postcondition failed after a rung answered (details.check); the message is a constant per type and the engine’s exception text never rides the wire (it stays in the service log under the response’s X-Request-Id) – internal_error – any other engine ValueError (details.exception_type), the engine’s fault and never the caller’s

  • 503 – suggest_timeout (on_timeout=fail past max_wait_s; Retry-After the estimate) | queue_full (every suggest permit of the instance held, prefix, parked or sending; Retry-After 1) | ask_deadline_exceeded (no Retry-After; details.point / stage / item_state / max_wait_s / backstop_s / elapsed_s / queue_wait_s / rung) | connection_pool_busy (Retry-After 1) – queue_full – every suggest permit of this instance is held (prefix, parked or sending; a reserve keeps slots for observations and reads), the ask never entered the queue; Retry-After 1; X-Tauopt-Admission: refused_parked – suggest_timeout – on_timeout is fail and the ask exceeded max_wait_s; Retry-After is the recent-ask estimate in seconds (at least 1) – ask_deadline_exceeded – the ask’s deadline: the engine’s caps-mode refusal (a config with timebox_mode: "caps" and an expired max_wait_s; a re-ask repeats deterministic work) or, on the queue lane, the service’s own backstop above the engine (details.point backstop: a hung ask ended past its bound, or the queue’s prefix bound); no Retry-After; details carries the engine’s point, max_wait_s, elapsed_s and rung and, on the service’s own backstop refusals (its queue lane’s), stage (prefix: the pre-queue work overran; wait: the ask was not served within the bound), item_state, backstop_s, backstop_source, queue_wait_s and item_id, as far as the ask got

DELETE /v1/experiments/{experiment_id}/suggestions#

Close All Suggestions. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 200 – Successful Response: {"closed": n} – every open suggestion is closed; archived experiments included

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – experiment_busy (another request holds the experiment; Retry-After 1) | connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1) – experiment_busy – another request holds the experiment’s lock and the 3 s bounded wait expired; Retry-After 1 (the ownership read precedes the lock, so a 404 never waits)

DELETE /v1/experiments/{experiment_id}/suggestions/{suggestion_id}#

Close Suggestion. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647), suggestion_id (integer; >= 1; <= 2147483647).

Responses:

  • 204 – Successful Response: no body; an already-closed suggestion answers 204 without change; archived experiments included

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown experiment or suggestion id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – experiment_busy (another request holds the experiment; Retry-After 1) | connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1) – experiment_busy – another request holds the experiment’s lock and the 3 s bounded wait expired; Retry-After 1 (the ownership read precedes the lock, so a 404 never waits)

GET /v1/experiments/{experiment_id}/summary#

Experiment Summary. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Path parameters: experiment_id (integer; >= 1; <= 2147483647).

Responses:

  • 200 – Successful Response: the web app’s one-call read, REST-only (the Python client has no method for it): best_so_far (the series), counts (observations, failed, open_suggestions, suggestions_served), frontier, terminal (budget_reached, exhausted) and revision – no experiment object and no progress

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – unknown id, or another user’s experiment

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/me#

Me. Authentication: API token, or the web app’s session cookie.

Responses:

  • 200 – Successful Response: {"email", "status", "created_at", "credential", "org": {"id", "slug", "name", "kind", "plan", "caps": {"org": {...}, "member": {...}}, "usage": {"suggests_24h", "active_experiments", "live_tokens"}}, "providers": [...]} – the account the credential belongs to, its organization’s plan with the caps the plan resolves to (null = unlimited: suggests_per_day, active_experiments, default_token_expiry_days, max_observations_per_experiment, timebox_off_allowed; per member max_tokens and inflight_per_user, the latter the effective in-flight bound the queue applies – the organization’s override, else 8) and the live usage against them; providers the sign-in methods linked to the account (each ProviderOut: id, linked_at, last_sign_in_at); credential is token under a tau_ token and session under the cookie

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

PUT /v1/me/providers#

Sync Providers. Authentication: the web app’s session cookie (a tau_ token answers 401).

Request body: ProvidersSyncIn (required).

Responses:

  • 200 – Successful Response: 200, ProvidersOut: the account’s sign-in methods after the sync, each ProviderOut with its id (email the primary, google.com), linked_at and last_sign_in_at; the request’s body, ProvidersSyncIn, is one fresh Identity Platform ID token whose firebase.identities is the new set (the web app’s link and unlink flows end here; a tau_ token or a REST client has no business with it); under the session cookie only

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field) – or the typed sync refusals in the same envelope: id_token_invalid (the token did not verify), identities_missing (no identities claim; never read as no providers), primary_identity_missing (no email identity: the primary is never unlinked here)

  • 401 – unauthorized (no live session: a tau_ token is never a management credential; the cookie is cleared)

  • 403 – origin_refused (a state-changing request under a session carries no allowed Origin) | subject_mismatch (the ID token names another identity subject than this account’s) – subject_mismatch – sign in again as the account the page shows; the sign-in methods are left as they were

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 409 – snapshot_stale (a newer snapshot of the sign-in methods was already synced – the ID token was minted before it; reload the page to see it; no row is written) – snapshot_stale – reload the page: the newer snapshot is the one shown, nothing was written

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/me/tokens#

List Tokens. Authentication: the web app’s session cookie (a tau_ token answers 401).

Responses:

  • 200 – Successful Response: {"data": [token, ...]} – the session user’s tokens, each id, prefix, name, org_id, audience, created_at, last_used_at, expires_at and never the value; under the session cookie only (a tau_ token answers 401: it is not a management credential)

  • 401 – unauthorized (no live session: a tau_ token is never a management credential; the cookie is cleared)

  • 403 – origin_refused (a state-changing request under a session carries no allowed Origin)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – token_quota_exceeded (the plan’s live-token cap per member; revoke one first) | rate_limited | credential_rate_limited – token_quota_exceeded – the document declares it on this route (the shared token error table); a list cannot answer it, only a mint can (POST /v1/me/tokens, or a rotate whose successor would exceed a cap lowered beneath the live count)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/me/tokens#

Mint. Authentication: the web app’s session cookie (a tau_ token answers 401).

Request body: MintIn (required).

Responses:

  • 201 – Successful Response: 201, the new token object with token, the raw value, in this answer only; the body is MintIn: an optional name (1..255 characters) and an optional expires_in_days (1..3650; absent = the plan’s default expiry, none on the beta); under the session cookie only

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (no live session: a tau_ token is never a management credential; the cookie is cleared)

  • 403 – origin_refused (a state-changing request under a session carries no allowed Origin) | account_pending | account_disabled (the status re-read under the mint’s lock)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – token_quota_exceeded (the plan’s live-token cap per member; revoke one first) | rate_limited | credential_rate_limited – token_quota_exceeded – the plan’s live-token cap per member (5 on the beta); revoke a token first

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

DELETE /v1/me/tokens/{token_id}#

Revoke. Authentication: the web app’s session cookie (a tau_ token answers 401).

Path parameters: token_id (integer).

Responses:

  • 200 – Successful Response: {"id", "revoked": true, "mcp_revocation_lag_s", "note"} – the token is refused by the API at once and by the MCP server within its verdict cache (mcp_revocation_lag_s reports the service’s own setting, 60, which the service pins equal to the MCP boundary’s positive-cache TTL); another user’s token id answers 404; under the session cookie only

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (no live session: a tau_ token is never a management credential; the cookie is cleared)

  • 403 – origin_refused (a state-changing request under a session carries no allowed Origin)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – not one of the caller’s tokens

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – token_quota_exceeded (the plan’s live-token cap per member; revoke one first) | rate_limited | credential_rate_limited – token_quota_exceeded – the document declares it on this route (the shared token error table); a revoke cannot answer it, only a mint can (POST /v1/me/tokens, or a rotate whose successor would exceed a cap lowered beneath the live count)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

POST /v1/me/tokens/{token_id}/rotate#

Rotate. Authentication: the web app’s session cookie (a tau_ token answers 401).

Path parameters: token_id (integer).

Responses:

  • 201 – Successful Response: 201, the successor token object with token (the raw value, shown once), revoked_id, and the revoke’s two fields mcp_revocation_lag_s and note (the old value is refused by the MCP server within its verdict cache): the same name, audience and expiry, the old value refused at once by the API; under the session cookie only

  • 400 – Validation Error (the validation_error envelope; the message names the field) – validation_error – a request-shape failure (the message names the field)

  • 401 – unauthorized (no live session: a tau_ token is never a management credential; the cookie is cleared)

  • 403 – origin_refused (a state-changing request under a session carries no allowed Origin) | account_pending | account_disabled (the status re-read under the mint’s lock)

  • 404 – not_found (the experiment, or the suggestion, is not the caller’s) – not_found – not one of the caller’s tokens

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 409 – token_expired (a rotation never lengthens a credential’s life: an expired token has nothing to rotate into – revoke it and mint a new one) – token_expired – an expired token has nothing to rotate into: revoke it and mint anew

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – token_quota_exceeded (the plan’s live-token cap per member; revoke one first) | rate_limited | credential_rate_limited – token_quota_exceeded – the document declares it on this route (the shared token error table); the successor is minted under the mint’s cap check with the rotated token excluded, so a rotate answers it only when a cap lowered beneath your live count already binds

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

GET /v1/usage/me#

My Usage. Authentication: API token, or the web app’s session cookie (every /v1 route takes either; the Python client sends a token).

Responses:

  • 200 – Successful Response: {"suggests_24h", "suggests_cap", "active_experiments", "active_cap", "plan"} – the two quota counters against their caps and the plan name (the same numbers as GET /v1/me’s usage and caps.org)

  • 401 – unauthorized (a missing, malformed, unknown, revoked or other-lane token)

  • 408 – request_timeout (the body not received within 30 s; the gate answers before the route)

  • 413 – payload_too_large (a body past 1 MiB; the gate answers before the route)

  • 429 – rate_limited (the beta lane’s buckets: the /v1 bucket, or /health’s own; Retry-After) | credential_rate_limited (the beta lane’s per-credential bucket, or its request concurrency; Retry-After 1)

  • 500 – internal_error (a crash: the constant envelope; the traceback stays in the server log)

  • 503 – connection_pool_busy (every pooled database connection on this instance is in use past the 5 s checkout wait; Retry-After 1)

Request body schemas#

Field order follows the service’s models. null as a default means the field may be omitted or sent as null. <= 2147483647 on an integer field is the service’s column unit (a PostgreSQL integer): a larger value answers 400 validation_error.

ExperimentIn#

Field

Type

Required

Default

Constraints

name

string

yes

--

length 1..255

parameters

array of ParameterIn

yes

--

1..256 items

metric

MetricIn | null

no

null

–

metrics

array of MetricIn | null

no

null

1..256 items

linear_constraints

array of LinearConstraintIn | null

no

null

<= 64 items

num_solutions

integer

no

1

>= 1; <= 2147483647

observation_budget

integer | null

no

null

>= 1; <= 2147483647

parallel_bandwidth

integer

no

1

>= 1; <= 2147483647

suggestion_ttl_s

integer | null

no

null

>= 0; <= 2147483647

metadata

object | null

no

null

–

Unknown fields are rejected with 400 validation_error.

ExperimentPatch#

Field

Type

Required

Default

Constraints

name

string | null

no

null

length 1..255

observation_budget

integer | null

no

null

>= 1; <= 2147483647

parallel_bandwidth

integer | null

no

null

>= 1; <= 2147483647

suggestion_ttl_s

integer | null

no

null

>= 0; <= 2147483647

metadata

object | null

no

null

–

metric_thresholds

object (string -> number | null) | null

no

null

<= 256 keys

Unknown fields are rejected with 400 validation_error.

LinearConstraintIn#

Field

Type

Required

Default

Constraints

terms

object (string -> number)

yes

--

<= 256 keys

op

">=" | "<="

yes

--

–

rhs

number

yes

--

–

Unknown fields are rejected with 400 validation_error.

MeOut#

Field

Type

Required

Default

Constraints

email

string

yes

--

–

status

"active" | "pending" | "disabled"

yes

--

–

created_at

string

yes

--

–

credential

"token" | "session"

yes

--

–

org

OrgOut

yes

--

–

providers

array of ProviderOut

yes

--

–

MemberCaps#

Field

Type

Required

Default

Constraints

max_tokens

integer | null

yes

--

–

inflight_per_user

integer

yes

--

–

MetricIn#

Field

Type

Required

Default

Constraints

name

string

yes

--

length 1..255

objective

"maximize" | "minimize"

no

"maximize"

–

strategy

"optimize" | "store" | "constraint"

no

"optimize"

–

threshold

number | null

no

null

–

Unknown fields are rejected with 400 validation_error.

MintIn#

Field

Type

Required

Default

Constraints

name

string | null

no

null

length 1..255

expires_in_days

integer | null

no

null

>= 1; <= 3650

Unknown fields are rejected with 400 validation_error.

MintOut#

Field

Type

Required

Default

Constraints

id

integer

yes

--

–

prefix

string

yes

--

–

name

string | null

yes

--

–

org_id

integer

yes

--

–

audience

string

yes

--

–

created_at

string

yes

--

–

last_used_at

string | null

yes

--

–

expires_at

string | null

yes

--

–

token

string

yes

--

–

ObservationIn#

Field

Type

Required

Default

Constraints

suggestion_id

integer | null

no

null

–

assignments

object (string -> number | integer | string) | null

no

null

<= 256 keys

value

number | null

no

null

–

values

object (string -> number) | null

no

null

<= 256 keys

value_stddev

number | null

no

null

–

values_stddev

object (string -> number) | null

no

null

<= 256 keys

failed

boolean

no

false

–

metadata

object | null

no

null

–

Unknown fields are rejected with 400 validation_error.

OrgCaps#

Field

Type

Required

Default

Constraints

suggests_per_day

integer | null

yes

--

–

active_experiments

integer | null

yes

--

–

default_token_expiry_days

integer | null

yes

--

–

max_observations_per_experiment

integer

yes

--

–

timebox_off_allowed

boolean

yes

--

–

OrgOut#

Field

Type

Required

Default

Constraints

id

integer

yes

--

–

slug

string

yes

--

–

name

string

yes

--

–

kind

"personal" | "team"

yes

--

–

plan

string

yes

--

–

caps

TaggedCaps

yes

--

–

usage

OrgUsage

yes

--

–

OrgUsage#

Field

Type

Required

Default

Constraints

suggests_24h

integer

yes

--

–

active_experiments

integer

yes

--

–

live_tokens

integer

yes

--

–

ParameterIn#

Field

Type

Required

Default

Constraints

name

string

yes

--

length 1..255

type

"double" | "integer" | "categorical"

yes

--

–

bounds

[number, number] | null

no

null

exactly 2 items

log_scale

boolean

no

false

–

categorical_values

array of string | null

no

null

<= 1024 items

grid

array of number | null

no

null

<= 4096 items

conditions

object (string -> array of string) | null

no

null

<= 64 keys

Unknown fields are rejected with 400 validation_error.

ProviderOut#

Field

Type

Required

Default

Constraints

id

string

yes

--

–

linked_at

string

yes

--

–

last_sign_in_at

string | null

yes

--

–

ProvidersOut#

Field

Type

Required

Default

Constraints

providers

array of ProviderOut

yes

--

–

ProvidersSyncIn#

Field

Type

Required

Default

Constraints

id_token

string

yes

--

length 1..8192

Unknown fields are rejected with 400 validation_error.

RevokeOut#

Field

Type

Required

Default

Constraints

id

integer

yes

--

–

revoked

boolean

yes

--

–

mcp_revocation_lag_s

integer

yes

--

–

note

string

yes

--

–

RotateOut#

Field

Type

Required

Default

Constraints

id

integer

yes

--

–

prefix

string

yes

--

–

name

string | null

yes

--

–

org_id

integer

yes

--

–

audience

string

yes

--

–

created_at

string

yes

--

–

last_used_at

string | null

yes

--

–

expires_at

string | null

yes

--

–

token

string

yes

--

–

revoked_id

integer

yes

--

–

mcp_revocation_lag_s

integer

yes

--

–

note

string

yes

--

–

SessionOut#

Field

Type

Required

Default

Constraints

user_id

integer

yes

--

–

email

string

yes

--

–

expires_at

string

yes

--

–

SuggestionIn#

Field

Type

Required

Default

Constraints

config

object | null

no

null

–

max_wait_s

number | null

no

null

> 0; <= 300

on_timeout

"ladder" | "fail"

no

"ladder"

–

intents

array of object | null

no

null

<= 64 items

tag

string | null

no

null

length 1..128

Unknown fields are rejected with 400 validation_error.

TaggedCaps#

Field

Type

Required

Default

Constraints

org

OrgCaps

yes

--

–

member

MemberCaps

yes

--

–

TokenList#

Field

Type

Required

Default

Constraints

data

array of TokenOut

yes

--

–

TokenOut#

Field

Type

Required

Default

Constraints

id

integer

yes

--

–

prefix

string

yes

--

–

name

string | null

yes

--

–

org_id

integer

yes

--

–

audience

string

yes

--

–

created_at

string

yes

--

–

last_used_at

string | null

yes

--

–

expires_at

string | null

yes

--

–

Request limits#

The bounds the service holds every request to:

  • Body size: a /v1 request body past 1048576 bytes (1 MiB) answers 413 payload_too_large (request body exceeds 1048576 bytes), from Content-Length when declared and from the streamed count otherwise; a body not received within 30 s answers 408 request_timeout (request body not received within 30 s). Both come from the pre-authentication gate, on every lane the service runs, outside the usage ledger.

  • One observation: a body past 32768 bytes serialized compactly answers 400 validation_error (observation body exceeds 32768 bytes serialized).

  • Observations per experiment: the organization’s plan’s max_observations_per_experiment (4000 on the beta plan, 2000 on the free plan); the next one answers 409 observation_cap_reached (details.cap, details.count), checked before the suggestion branch so an open suggestion stays open. Archive the experiment and start a new one, dismiss what you cannot report, or, with repository access or at the public release, run the local driver.

  • Structure: the schema tables above carry the caps – 256 parameters, metrics, assignment and value keys, constraint terms, and thresholds; 64 linear constraints, condition keys, and intents; 1024 categorical values; 4096 grid points; names of 1 to 255 characters.

  • Ids and the list window: experiment_id and suggestion_id are 1..2147483647 and limit and offset 0..2147483647, the column unit; a value past them answers 400 validation_error.

  • Locks: the four routes that take an experiment’s lock (PATCH and DELETE on the experiment, the two DELETEs on its suggestions) wait at most 3 s for it and then answer 503 experiment_busy with Retry-After 1; the ownership read precedes the lock, so a 404 never waits. An ask takes the lock on its worker, behind the queue: a wait that times out there puts the ask back in the admitted set and the caller never sees it. The active-experiment quota check holds the user’s row for the create and waits at most 3 s for it, answering 429 quota_unverifiable past that.

Error shape#

Every non-2xx response is one JSON object – the service’s own refusals, the framework’s (http_error), and a crash (500 internal_error) alike:

{"error": {"type": "<error_type>", "message": "<text>"}}

details is present when the error carries a machine-readable payload; the Error types table below names the keys per type (the dedup 409 answers, the typed 500 answers, the 503 ask_deadline_exceeded, the 409 suggestion_expired and observation_cap_reached, and the admission and idempotency refusals). The exhaustion answer:

{"error": {"type": "space_exhausted", "message": "<text>", "details": {"dedup": {"exhaust_proof": true, "proof_method": "enumeration"}, "terminal": true}}}

details.dedup is the engine’s dedup record (counters, ladder hops, proof fields, intent refusals); details.terminal is true only on space_exhausted. Request-shape failures (pydantic validation of the body, path, or query) answer 400 with {"error": {"type": "validation_error", "message": "<field>: <reason>"}}. The OpenAPI document declares that 400, as the ApiError schema below, on every route with parameters or a body, and no 422 anywhere: the service’s request-validation handler answers 400 in the shape above, never the framework’s 422. Path ids and the list window are bounded at the column unit (experiment_id and suggestion_id 1..2147483647; limit and offset 0..2147483647), and a body carrying a NUL byte, a lone surrogate, or a non-finite number is refused before field parsing with one message per class.

Engine errors and crashes. The service maps every engine error it knows: AskValidationError to 400 validation_error; the caps-mode AskDeadlineExceeded (POST /v1/experiments/{experiment_id}/suggestions with a config carrying timebox_mode: "caps" and an expired max_wait_s) to 503 ask_deadline_exceeded with no Retry-After and details carrying point, max_wait_s, elapsed_s, and rung; AskFailed to 500 all_rungs_failed or integrity_violation; any other engine ValueError to 500 internal_error with details.exception_type. Anything else that escapes a route answers 500 internal_error with the constant message internal error and no details, inside the envelope; the framework’s own refusals (an unknown path, a method the route lacks, a body it cannot parse) answer http_error at the framework’s status.

ApiError#

Field

Type

Required

Default

Constraints

error

ApiErrorBody

yes

--

–

ApiErrorBody#

Field

Type

Required

Default

Constraints

type

string

yes

--

–

message

string

yes

--

–

details

object | null

no

null

–

Error types#

Every error_type the service raises deliberately, found by scanning its app package for the raise sites (an engine error the service does not map is the 500 internal_error of Engine errors and crashes under Error shape); the Python client maps these onto exception classes (Errors). Retry-After says whether the answer carries that header.

HTTP status

error_type

Retry-After

Raised by

Meaning

400

id_token_invalid

no

app.api.v1.me_providers

PUT /v1/me/providers (a web-app route): the ID token did not verify; 400 in the validation envelope – sign in again and retry

400

identities_missing

no

app.api.v1.me_providers

PUT /v1/me/providers (a web-app route): the ID token carries no identities claim (never read as no providers); 400 in the validation envelope, no row written

400

invalid_cursor

no

app.admin.routes

the admin API only (/admin/api/*, outside this page): an events page’s before names no row – pass the id of the last row you saw; 400

400

primary_identity_missing

no

app.api.v1.me_providers

PUT /v1/me/providers (a web-app route): the ID token’s identities carry no email – the primary sign-in method is never unlinked here; 400 in the validation envelope, no row written

400

validation_error

no

app.admin.routes, app.admin.writes, app.api.v1.experiments, app.api.v1.observations, app.api.v1.suggestions, app.errors, app.service.suggest, app.worker.run

the request shape (a pydantic failure naming the field), an id or list-window value past the column unit, a body defect (a NUL byte, a lone surrogate, a non-finite number), an observation past 32768 bytes serialized, or an engine space, metric, config, or intent rule; the message carries the reason

401

unauthorized

no

app.admin.auth, app.api.v1.auth_session, app.auth, app.preauth, app.queue.admission

no credential at all (no X-Tauopt-Token, no Authorization: Bearer header – the scheme name is case-insensitive, RFC 9110 – and no session cookie), an unknown or revoked token (revocation deletes the token row, so the two are the same lookup miss), a token issued for a different lane, or – at the pre-authentication gate, with no lookup – a repeated credential header field or a token outside the minted shape

403

account_disabled

no

app.api.v1.auth_session, app.tokens

the bootstrap route: the account is disabled (users.status disabled); the session cookie is cleared

403

account_pending

no

app.api.v1.auth_session, app.tokens

the bootstrap route (POST /v1/auth/session): the account exists but awaits approval (users.status pending); the session cookie is cleared; no sign-in until an operator activates it

403

forbidden

no

app.admin.auth

the admin API’s door only (/admin/api/*, a separate document outside this page); the web app’s own 403s are origin_refused, account_pending, account_disabled and subject_mismatch (their own rows): with an IAP assertion, the verified Google identity is outside the configured admin set, has no users row, has several users rows under its lowercased email, or has a role other than admin; without one, a resolved API token was presented where only the assertion is a credential (an unresolved or shapeless token stays the 401); the message names the remedy; never on a /v1 route

403

origin_refused

no

app.admin.auth, app.preauth

the pre-authentication gate: the request carries an Origin outside the service’s web-origin allow-list on a route that reads one (the browser’s session routes); answered before the body is read

403

subject_mismatch

no

app.api.v1.me_providers

PUT /v1/me/providers (a web-app route): the fresh ID token’s subject is not the signed-in account’s identity; the sign-in methods are left as they were

404

not_found

no

app.admin.routes, app.admin.twins, app.admin.writes, app.api.v1.me_tokens, app.api.v1.observations, app.api.v1.suggestions, app.service.locks

an unknown experiment or suggestion id, another user’s id, or (on mutating routes) an archived experiment

408

request_timeout

no

app.preauth

the request body was not received within 30 s; answered by the pre-authentication gate

409

conflict

no

app.api.v1.observations

the suggestion already has an observation

409

create_user_unique_email

no

app.admin.writes

the admin API only (/admin/api/*, outside this page): an invite for an email that already has a users row; 409, no row written

409

idempotency_key_mismatch

no

app.service.suggest

the Idempotency-Key header of this ask was used against ANOTHER experiment (the header’s length is checked first: a 400 validation_error past 128 bytes); details.experiment_id names it; a key is scoped to the user and the experiment

409

observation_cap_reached

no

app.api.v1.observations

the experiment holds its organization’s plan’s max_observations_per_experiment (4000 on the beta plan, 2000 on the free plan); details.cap and details.count; archive it and start a new experiment, dismiss what you cannot report, or, with repository access or at the public release, run the local driver

409

self_status_change

no

app.admin.writes

the admin API only (/admin/api/*, a separate document outside this page): an operator never changes their own status from the console; 409, no row written

409

snapshot_stale

no

app.api.v1.me_providers

PUT /v1/me/providers (a web-app route): the ID token was minted before the latest accepted snapshot of the account’s sign-in methods – a newer sync already landed; no row is written; reload the page to see it

409

space_exhausted

no

app.worker.run

every input-feasible point has been suggested or observed; details.dedup carries the proof and details.terminal is true

409

suggestion_expired

no

app.api.v1.observations

the suggestion was closed by the TTL expiry before the observation arrived; details carries suggestion_id, expired_at, created_at, and assignments for an unsolicited re-post

409

token_expired

no

app.api.v1.me_tokens

POST /v1/me/tokens/{token_id}/rotate on a token whose expires_at has passed: nothing to rotate into – revoke it and mint anew

409

unique_candidate_unavailable

no

app.worker.run

no unique candidate was found and exhaustion is not proven; details.dedup carries the record

413

payload_too_large

no

app.preauth

the request body is past 1048576 bytes, by Content-Length or by the streamed count; answered by the pre-authentication gate

429

bandwidth_full

yes

app.service.suggest

open suggestions already equal parallel_bandwidth; Retry-After 1

429

credential_rate_limited

yes

app.auth, app.ratelimit

beta lane only: the admission layer’s per-credential token bucket, charged to a shape-valid credential before the lane bucket (a first-seen digest is charged to one shared newcomer bucket first), is empty, or the credential is past its request concurrency cap; a bucket refusal is held for the refusal tarpit before it answers; Retry-After 1

429

experiment_quota_exceeded

no

app.quota

active experiments reached the organization’s active_experiments (its plan’s value – 25 on the beta plan – or the override); no Retry-After – archive an experiment first

429

quota_exceeded

yes

app.quota

admitted suggestions in the rolling 24-hour window reached the organization’s suggests_per_day (its plan’s value – 1000 on the beta plan – or the organization’s override); Retry-After is the time until the oldest counted suggestion leaves the window

429

quota_unverifiable

yes

app.quota

the quota store could not be read, or the create’s wait for the user’s row (the serialized active-experiment count) passed 3 s, so the request is refused rather than admitted unchecked; Retry-After 30

429

rate_limited

yes

app.ratelimit

beta lane only: the per-instance pre-authentication token bucket (150 requests per second, burst 300) on every /v1 path is empty, or /health’s own bucket (5 per second, burst 10) is; Retry-After 1

429

suggest_rate_limited

yes

app.suggest_rate

the per-user suggest rate (about ten a second sustained, a burst of twenty, every user; consumed before the idempotency lookup and the validation refusals, so refused asks count): slow the loop down or batch the asks; Retry-After 1; details carries rate_per_s and burst

429

token_quota_exceeded

no

app.tokens

POST /v1/me/tokens when the caller’s live tokens in the organization already equal the plan’s max_tokens (5 on the beta plan; null = unlimited), and a rotate only when a cap lowered beneath the live count already binds (the rotated token is excluded from the count); no Retry-After – revoke one first

429

too_many_inflight_asks

yes

app.service.suggest

the queue front: a retry (the same Idempotency-Key) already waits on one of the user’s in-flight asks past the per-user waiter cap – answered at once, Retry-After 1; details carries inflight, inflight_limit, queued and queued_limit (the user’s joined waiters and their cap, 1) and joined: true

429

too_many_parked

yes

app.service.suggest

the queue front: the user holds its share of this instance’s suggest permits (the handlers parked for an answer) beside the other tenants on it, or its allowance alone; answered at once, Retry-After 1, outcome refused_share; details carries parked, parked_share and max_parked

429

too_many_queued

yes

app.queue.admission

the queue front: the user’s own queue already holds queued_limit asks waiting behind its in-flight bound (the organization’s override, else the service default of 8; the plan’s inflight_per_user column is informational since deploy 2 retired the inline lane); answered at once, Retry-After 1, outcome refused_queue in X-Tauopt-Admission; details carries inflight, inflight_limit, queued and queued_limit

500

all_rungs_failed

no

app.worker.run

the engine’s AskFailed: every rung of the fallback chain failed; a constant message, details.intended and details.attempts – each attempt’s rung, stage and exception_type, never the exception text, which stays in the service log under the response’s X-Request-Id

500

integrity_violation

no

app.worker.run

the engine’s AskFailed: a postcondition failed after a rung answered; a constant message, details.check names it

500

internal_error

no

app.errors, app.service.suggest, app.worker.run

an engine ValueError that is not the caller’s (details.exception_type), or any other exception that escapes a route (the constant message internal error, no details)

503

ask_deadline_exceeded

no

app.service.suggest, app.worker.run

the ask’s deadline: the engine’s caps-mode refusal (timebox_mode: "caps" and an expired max_wait_s; a re-ask repeats deterministic work) or the service’s own backstop above the engine on its queue lane (details.point backstop: a hung ask ended past its bound, or the queue’s prefix bound); no Retry-After; details carries the engine’s point, max_wait_s, elapsed_s and rung and, on the service’s own backstop refusals (its queue lane’s), stage, item_state, backstop_s, backstop_source, queue_wait_s and item_id, as far as the ask got

503

connection_pool_busy

yes

app.api.v1.suggestions, app.errors

every pooled database connection on this instance is in use past the 5 s checkout wait (the pool’s bounded pool_timeout); Retry-After 1; any route

503

experiment_busy

yes

app.service.locks

another request holds the experiment’s advisory lock and the 3 s bounded wait expired; Retry-After 1

503

queue_full

yes

app.service.suggest

the queue front: every suggest permit of this instance is held (prefix, parked or sending a response; a reserve keeps slots for observations and reads) – the ask never entered the queue; answered at once, Retry-After 1, outcome refused_parked

503

suggest_timeout

yes

app.worker.run

on_timeout is fail and the ask exceeded max_wait_s; Retry-After is the recent-ask estimate

the framework’s (404, 405, 400)

http_error

no

app.errors

the framework’s own refusal inside the envelope: 404 for a path that is no route, 405 for a method the route lacks, 400 for a body the framework cannot parse

Authentication#

Every experiment and account route under /v1 takes an API token or the web app’s session cookie (the web app’s own reads ride the cookie); the seven session routes take the web app’s own credentials – the sign-in bootstrap a fresh Identity Platform ID token; the sign-out, the sign-in-methods sync (PUT /v1/me/providers) and the token management under /v1/me/tokens the session cookie – and a tau_ token there answers 401; /health takes nothing. Tokens start with tau_, are shown once when issued, and are stored and compared as SHA-256 hashes. The service reads the token from, in order:

  1. X-Tauopt-Token: <token> – checked first; the header for deployments behind an identity-aware proxy that owns Authorization.

  2. Authorization: Bearer <token> – used when X-Tauopt-Token is absent or blank. The Python client sends this header. The scheme name is case-insensitive on every door (RFC 9110 section 11.1); the separator and the token’s shape are not relaxed.

A third credential class exists for the web app alone: the browser’s session cookie (__Host-tauopt_session, minted by POST /v1/auth/session from a fresh Identity Platform ID token). It is never read beside a token (two credentials answer 401 invalid token), and the token self-service routes under /v1/me/tokens and the sign-in-methods sync PUT /v1/me/providers take it only.

A request that presents no credential – no X-Tauopt-Token, no Authorization: Bearer header and no session cookie – answers 401 unauthorized with the message missing Bearer token. A token that is unknown or revoked (revocation deletes the token row, so the two are the same lookup miss), or that was issued for a different lane of the service, answers 401 unauthorized with the message invalid token; the cases are indistinguishable by design. On the public beta lane every /v1 request first passes a per-instance token bucket (150 requests per second, burst 300) before authentication; an empty bucket answers 429 rate_limited with Retry-After 1.

Before the body is read and before any lookup, on every lane the service runs, a pre-authentication gate refuses three shapes: no credential at all (no token header and no session cookie; on the sign-in bootstrap, no ID-token bearer – a cookie there is not counted) answers the 401 missing Bearer token above without reading the body; a credential header field that is repeated (two Authorization fields, or two X-Tauopt-Token fields – one of each is the proxy posture and passes) or a token outside the minted shape (tau_ followed by 43 URL-safe characters) answers the 401 invalid token above with no lookup, indistinguishable from an unknown token by design.