Back to Documentation

API Changes

Changes to the Taiso API that a client may notice. Each entry says what a request now gets back, so you can check your error handling against it.

From API version 1.21.0

Check which version you are talking to with GET /api/v2/version:

curl https://api.taiso.ai/api/v2/version

The newest changes come first, and each names the version it shipped in. The sections after them, from “404 if you cannot see it” on, shipped in 1.21.0.

The answer says why the model stopped (from API version 1.38.5)

  • POST /api/v2/llm/chat now returns finish_reason, as the provider reported it: stop, or length when max_tokens ran out. A reasoning model can spend the whole budget before writing, so an empty message.content with length means: raise max_tokens.
  • POST /api/v2/llm/completions and POST /v1/chat/completions report the provider's finish_reason instead of always stop, so an OpenAI client sees length when an answer was cut off. On /v1, an enhanced answer (one with a taiso block) still reports stop.

Intent extraction errors match the LLM endpoints (from API version 1.38.4)

  • POST /api/v2/tools/intents/extract no longer answers 500 when the model call fails. When no provider serves the requested model, it answers 503 "No configured provider can serve this request right now". When the provider rejects the call, it answers as POST /api/v2/llm/chat does: 400 "bad_request", 404 "model_not_found", 429, 502 or 503. The detail no longer repeats the provider's or our configuration's error text.

Removed self-test routes and search-engine fields (from API version 1.38.0)

  • GET /api/v2/tools/calc/test and GET /api/v2/tools/expression/test are gone (404). They ran built-in examples, not your input; use POST /api/v2/tools/calc and POST /api/v2/tools/expression.
  • Search has run on PostgreSQL alone for months, so the fields that described a separate search engine are gone: search from GET /api/v2/health, elasticsearch_available from search responses, and elasticsearch_indexed from file responses. GET /api/v2/health now answers "status": "ok" whenever the database is ok; it used to say "degraded" because of that engine.
  • POST /api/v2/sops/generate-sop-schema, POST /api/v2/sops/generate-decision-block and POST /api/v2/agents/generate-agent-schema now stop at the same 110-second deadline as the structured-block generators and answer 504 "deadline_exceeded", instead of retrying after the connection had already timed out.
  • A v2 SOP node whose next points at itself is reported as the cycle a -> a (with cycle_path ["a", "a"]), not a -> a -> a. Other cycles are reported as before.

Limits on one request's work (from API version 1.37.0)

  • A regular expression in text-search, text-replace, text-extract, a form or DOCX extraction schema, or a JSON Schema pattern now has 2 seconds on its text. Past that it is stopped: 422 from the text tools, a field error from the extractors, a failed validation in structured blocks. A pattern over 2,000 characters, or a replacement whose result would pass 20 million characters, answers 400. In a JSON Schema this covers every pattern, including the matching inside additionalProperties and unevaluatedProperties; a schema you pass that is not valid (say, a pattern that does not compile) answers 400 "invalid_schema" before any model call. The form and DOCX extractors take at most 200 fields, whose patterns share 5 seconds per request.
  • A JSON Schema in structured blocks (json_schema, data_schema, and the schema generate-block-schema writes) resolves $ref and $dynamicRef only within itself and the published JSON Schema meta-schemas. A URL is never fetched. A reference that does not resolve, or a $schema that is not a string, answers 400 "invalid_schema" before any model call; a model-written schema with one is retried. A pattern nested too deeply to parse is also a 400.
  • taiso.policy pattern lists on /v1/chat/completions and /api/v2/llm (forbid_arguments, forbid_in_output, redact_in_output) must be lists of valid patterns, at most 2,000 characters each and 100 in all, or the request answers 400. At run time each match has 0.25 seconds and a request's policy checks share 10 seconds of CPU; one that runs out of time refuses rather than allows.
  • A decision expression whose result would be too large (a list or string over a million elements or characters, counted through nested lists; a % width or precision over 10,000 or given as *, also after a key as in %(n)d; a replace() past that size) is an expression error, and the decision takes its default branch. POST /api/v2/tools/expression applies the same % limits and answers 400. sum() adds numbers only.
  • PDF, DOCX and HTML files are read in a separate process with a memory and time limit. A document that cannot be read within them answers 400 (in a job: a failed step). A DOCX whose contents expand past 300 MB is refused unread. Pages are rendered at no more than 50 million pixels whatever DPI is asked for, and a file job's dpi is capped at 600. convert-to-pdf answers 422 past 2,000 pages, and its options.margin must be points or a length in in (or inch, inches), mm, cm or pt, from 0 to 40% of the page's shorter side (244.8 pt on Letter). Absent or "" means one inch; anything else answers 400, where an unreadable value used to mean one inch.
  • text-diff answers 422 past 500 lines per text or 50,000 characters for ndiff and html (or when one takes more than 10 seconds: use unified for large or very different texts), and past 50,000 lines or 5 million characters otherwise. Its changes_summary counts are unchanged. text-replace with return_diff follows the unified limits.
  • web-crawl-simple: timeout is 1 to 30 seconds (422 otherwise) and covers the whole fetch, redirects included. A page over 10 MB answers 413, one that takes more than 10 seconds to convert 422. text and markdown no longer include script contents. A successful page in an encoding other than gzip or deflate, or a corrupt compressed one, answers 502; a redirect's body is not read, and an error page that cannot be decoded comes back empty with its status.
  • A structured-block request's model calls now stop at its deadline, raised from 100 to 110 seconds (504 deadline_exceeded), instead of a call started before it running its whole timeout past it.

Webhooks, invitations, agents and keys (from API version 1.37.0)

  • A webhook delivery must finish within 15 seconds in all, and at most 4 KB of your answer is read; only its status code counts. Answer quickly and do the work afterwards. A redirect (3xx) is not a delivery: it is retried like any other failure. Staging's webhooks are now signed with staging's own key, not the public development key.
  • Opening an invitation link (GET /api/v2/invitations/<token>) shows a confirmation page and spends nothing, so mail scanners and link previews cannot redeem it. POST to the same URL redeems it and returns the key once, with Cache-Control: no-store.
  • New or changed agent YAML may not use aliases (*name) and may be at most 256,000 characters; either answers 400. Agents saved with aliases before this version keep running and compiling. Changing an agent's definition pauses its schedules that belong to anyone else, with a paused_reason; they review the agent and resume them. A run such a schedule queued before the pause does not start.
  • An agent's compiled SOP changes only through the agent: PUT, PATCH, rollback and DELETE on /api/v2/sops/<id> answer 409 with error type "compiled_for_agent", naming the agent. Change it with PUT /api/v2/agents/<id>, or delete the agent, which deletes its SOP.
  • POST /api/v2/structured-blocks/generate-schema and …/generate-block-schema stop at the request deadline with 504 "deadline_exceeded", as …/generate and …/transform do.
  • The MCP ask_question tool needs the assist:write scope, as POST /api/v2/assist/ask does.
  • PATCH /api/v2/users/me sets a new email only with an admin key; with any other credential it answers 403 (addresses change at taiso.ai, where they are verified).
  • GET /api/v2/sops/categories counts public SOPs only.
  • GET /api/v2/llm/models lists deepseek/deepseek-r1-0528 in place of deepseek/deepseek-r1, which OpenRouter no longer serves (requests for it answer model_not_found). Switch any SOP or request that names the old id.
  • Issuing a key with "unique_name": true refuses a second active key of the same name with 409, detail.code "name_taken" and the existing key's key_id, before any key-limit refusal. Expired keys and keys made for agent runs do not hold a name. The Hub's service-key route sends it.
  • A conversation started by one user (conversation_id on the generate-schema routes) can no longer be continued by another; their id starts a new conversation.

Keys issued by a service (from API version 1.36.0)

  • A partner service that issues one key per customer on its own account now does it through the Hub, with its named service key: POST and GET /api/service/users/<id>/api-keys, DELETE …/api-keys/<key id> to revoke, and PUT …/api-keys/<key id>/rate-limit. Its key needs keys:read or keys:write and reaches only the accounts listed for it; any other account answers 403.
  • A key can be issued with its own rate_limit_per_minute (at most the account plan's) and its own monthly and daily spending caps. A second active key with the same name answers 409 with the first one's key_id, so a retry cannot issue two.
  • DELETE /api/business?id=… deletes a business the calling key created, while nobody belongs to it (409 otherwise; 404 for another caller's). A named key listing businesses sees its own, plus those from before creators were recorded.

Streaming (from API version 1.35.0)

  • POST /v1/chat/completions and POST /api/v2/llm/completions with stream: true (and no verification) now stream, as OpenAI's chat.completion.chunk events ending in data: [DONE]. They used to accept stream: true and return one JSON body. Add "stream_options": {"include_usage": true} for a final usage chunk.
  • A streamed call that fails before its first token, on these routes or POST /api/v2/llm/chat, now answers with the same status and JSON error as without streaming (404, 429, 503...). It used to be a 200 stream holding an error event.
  • The last event of a /api/v2/llm/chat stream carries finish_reason, and the role is sent once rather than on every event.
  • POST /api/v2/llm/simple-json with stream: true answers 422; it used to ignore it.
  • A stream ends after 10 minutes, with an error event of type llm_timeout.
  • A bad sampling parameter ("max_tokens": "lots") on POST /v1/chat/completions or POST /api/v2/llm/completions answers 400, streamed or not. It used to be 503, which OpenAI's SDKs retry.

Webhooks (from API version 1.35.0)

  • Every webhook is signed: X-Taiso-Signature: t=<unix seconds>,v1=<hex>, HMAC-SHA256 of "<t>.<raw body>" with your secret, which is on your API Keys page (or GET /api/v2/webhooks/signing-secret; rotate it there or with POST /api/v2/webhooks/signing-secret/rotate). Receivers that ignore the header are unaffected.
  • Every payload gains delivery_id (also the X-Taiso-Webhook-Id header, the same on every retry: dedupe on it), occurred_at and sequence.
  • A cancelled job now sends job.cancelled. A SOP or agent run cancelled directly now sends sop_run.cancelled or agent_run.cancelled (it sent nothing), and a strict SOP run stopped for missing inputs sends sop_run.incomplete. A receiver that expects only completed and failed should ignore other events.
  • A record that ends is announced exactly once, and a retry sends the same body as the first attempt.
  • A webhook_url must be https on port 443. Another scheme or port is refused with 400 when the job or run is created, and a stored URL that breaks the rule is not delivered to.

Key scopes (from API version 1.35.0)

  • Scopes given to a key when it is made now limit it: <area>:read for an area's GET routes, <area>:write for the rest (see API reference). A key outside its scopes will get 403; for now such calls are only logged, and refusing them follows after a notice here.
  • Keys made without scopes, which is every key made at taiso.ai, keep full access. An unknown scope is refused with 422 when a key is made.

Code runs in an isolated runner (from API version 1.33.0)

  • Programs sent to POST /api/v2/code/stateless/python/exec now run on a separate, isolated runner, never inside the API. A program has no network, sees only its own working directory, and its environment holds only the runner's own variables (PATH, HOME, TMPDIR and a few Python settings). A program that relied on anything else will now fail.
  • Two new 503 answers from POST /api/v2/code/stateless/python/exec: detail.error is runner_busy, with Retry-After: 5, when every runner slot is in use (retry then), or runner_unavailable when no runner is reachable. Code never falls back to running in the API.
  • The same runner runs the calculations a knowledge-pack smart search makes, and the code checks Lodestar runs for a request with a taiso block (POST /api/v2/llm/chat, POST /v1/chat/completions). Without a runner, a smart search does not calculate, and such a code check fails with “code execution is not available on this deployment”.

Spending caps, keys and errors (from API version 1.33.0)

  • A key over a spending cap is refused (429) only by requests that can spend. GET requests still work, so you can fetch a result you have already paid for or list your projects. Model calls check the caps whatever the route.
  • Creating a key past your limit (POST /api/v2/users/me/api-keys) answers 409 with the same structured detail as the admin route: code: "key_limit", limit, active, ceiling and message. It used to be a plain string.
  • A malformed sop_ref on POST /api/v2/sops/{sop_id}/invoke (an empty id, a version that is not a positive number, extra parts) answers 422 rather than 500.
  • Structured Blocks (/generate and /transform): every retry of one request now fits in 100 seconds. Past that the answer is 504 with detail.error deadline_exceeded; try a smaller input. 503 now means only that no provider can serve the model you asked for. Any other failure is a 500 that names the kind of failure, and no error quotes a provider's reply any more.
  • POST /api/v2/sops/generate-sop-schema: a provider that refuses our account for lack of credit (402) answers 502 provider_auth_error, and last_error no longer carries the provider's reply.
  • GET /api/v2/llm/models lists only models a chat request can use: OpenAI's realtime, computer-use, image, speech and transcription models, and SambaNova's Whisper and E5 models, are gone. Two backends that serve the same model now list each other under also_via, not only OpenRouter.
  • The public GET /api/v2/health names a database failure only by its kind ("database": "error: <kind>"), no longer by its message.

Agents, audits and knowledge packs (from API version 1.33.0)

  • The SLAF audit fails a block for contradicting its source only when it quotes the contradicting text from the source. A disputed verdict without such a quote no longer fails the block, and POST /api/v2/structured-blocks/audit reports it as unclear.
  • Agents can choose how they are audited, as an SOP invocation can: settings.execution_mode (strict, the default; relaxed, no audit; or custom) and settings.execution_config.audit_gate. Each audit is a model call and is billed as one.
  • Knowledge packs (POST /api/v2/tools/knowledge_pack): max_nuggets defaults to 32 and goes up to 100. It was 8 and 32 before API version 1.26.0, a change this page did not list then. Ask for 8 if you relied on the old default.
  • corroborations now counts distinct sources: one section repeating a claim counts once.
  • When too many knowledge-pack searches are running at once, a search answers 503 with detail.error.type busy and Retry-After: 5, rather than waiting without limit.

For administrators (from API version 1.33.0)

  • POST /api/v2/admin/users/{id}/invitations needs admin:keys:write, since claiming an invitation makes a key; inviting an administrator also needs admin:users:grant_admin. PATCH /api/v2/admin/users/{id} with status: "deleted" needs admin:users:delete, as DELETE does.
  • An admin key stored with no scopes now has none; it used to have all of them. Ordinary keys without scopes are unchanged.
  • A user can be created as a test account only with an address on a reserved domain (example.com and the like, .test, .invalid, test.taiso.ai) or a tooling address; otherwise 422. Giving a test account a person's email answers 409.
  • Purging a user keeps their Taiso account id, so a later sign-in with the same identity is refused (403) instead of creating a new account; creating a user with that id answers 409. The purge also removes their IP address and email from the admin audit trail, which then reads [erased].
  • GET /api/v2/admin/usage/rollups?format=csv puts a quote before any text cell that starts with =, +, -, @, a tab or a carriage return, so a spreadsheet does not run it as a formula. Numbers are unchanged.

Plans and billing (from API version 1.32.0)

  • A change to your plan reaches the API as soon as it is made: the plan's API key limit applies to POST /api/v2/users/me/api-keys straight away, rather than after the nightly copy.
  • Plans no longer list SOP executions a month or blocks per SOP: nothing enforced either. A plan lists only limits the API enforces. When Taiso switches plan spending caps on, an account's calls past its plan's monthly spending cap answer 429, like any spending cap, until the 1st (UTC); your plan's page on the Billing tab shows its cap once it applies.
  • For administrators: GET /api/v2/admin/usage/rollups adds invoiceable_cost_usd (billing cost less estimates, which are never invoiced) to every group and the totals, stale_days (days a cost correction has changed since they were rolled up) and closed_periods; complete is now false while a day is stale. GET /api/v2/admin/usage/periods/{period} (scope admin:usage:read) says whether a month can be closed for invoicing, and POST /api/v2/admin/usage/periods/{period}/close (scope admin:usage:write) closes it; afterwards its costs no longer change unless forced. A closed month's state shows any change since the close, overall (changed_since_close) and per account (changed_accounts); a second close of the same month answers with already_closed: true.
  • For administrators: purging a user who is not a test account (DELETE /api/v2/admin/users/{id}?purge=true) still anonymises them, but now keeps their business_id. It names an account, not the person, and their usage stays on that business's invoices.

Model call errors (from API version 1.32.0)

  • POST /api/v2/structured-blocks/generate-schema, /generate and /transform answer 502 rather than 500 when the model gives no usable reply: detail.error is llm_output_truncated (cut off at the model's output limit: ask for shorter output), llm_empty_reply or llm_invalid_json. A provider failure gets the same answer as on /api/v2/llm: 502 provider_unavailable, 429 rate_limited and so on; a spending cap reached during the call is its 429. Retry a 502 later; a 500 now always means our failure.
  • POST /api/v2/structured-blocks/audit and /generate-block-schema answer a provider failure the same way. /audit used to answer 500, and /generate-block-schema 422 GENERATION_FAILED.
  • Long transforms no longer fail early: the model may now write up to 8,192 tokens, where it had 2,048. A reply cut off is retried once at double that, if the request has time left. A very long input leaves itself room in the model's context, never under 2,048 tokens of output, so it is not refused for going over the context.
  • On /api/v2/llm and Structured Blocks, a provider call that timed out or lost its connection on every retry answers 502 provider_unavailable rather than 500.
  • POST /api/v2/sops/generate-sop-schema retries a provider only when that may help: a timeout, a lost connection, 408, 429 or a 5xx. A request the provider refuses (too long, say) answers 422 GENERATION_FAILED at once, with stage: "llm_error" and the provider's status in provider_status. When the provider refuses our account, the answer is 502 provider_auth_error, not your error.

Usage and costs (from API version 1.31.0)

  • total_requests on your API keys (GET /api/v2/users/me/api-keys) counts HTTP requests the key made, once each. It used to add one more for every model call a request made, so a key mostly used for model calls shows a lower number from now on. Model calls are in your usage instead.
  • Tokens a provider read from or wrote to its prompt cache, and reasoning tokens, are priced at their own rates: a cached prompt costs less than before, as the provider bills it. cost_cents is rounded to the nearest cent on every route (some routes used to round down).
  • For administrators: GET /api/v2/admin/logs returns the last 7 days unless you pass since and until; its total stops counting at 10,000 (total_capped); and ip_address is returned only with include_ip=true. GET /api/v2/admin/usage/rollups reports usage per account, user, key or day.

How many API keys you can hold (from API version 1.30.0)

  • Your plan sets how many active API keys you can hold: 10 on the free plan, and no limit on Enterprise. A safety limit of 1,000 active keys applies to every account. Creating another, through the API or this site, answers 409 until you revoke one.
  • A plan's own limit reaches POST /api/v2/users/me/api-keys once the plan has been copied to SOP Engine; until then the API holds every account to the free plan's 10, while this site applies your plan.
  • Keys made for scheduled runs, and expired keys, do not count; GET /api/v2/users/me/api-keys marks them with agent_run_id and expired.

A block that contradicts its source fails (from API version 1.28.2)

  • In strict mode, the default and the mode agents run in, the SLAF audit is now a gate. If it finds that a block's output contradicts its source (a field it classifies disputed), the block is generated and audited once more. If the second attempt is disputed too, the block fails, and the run fails with an error naming the fields: Block 'transform_data' failed its SLAF audit on all 2 attempts: document_title contradicts the source. Before, the run completed with the contradicting output.
  • A block that passes on its second attempt lists the first attempt's findings under audit_retry in its output. The run's trace keeps a failed block's rejected output and its audit.
  • A field the audit could not find in the source (not_found) does not fail a block: a report or a judgment is not in its source by nature. An audit that could not run never fails a block.
  • In custom mode, execution_config.audit_gate chooses the gate: "off", "disputed" (the default) or "failed" (the audit's overall pass mark, which also counts fields it could not find). Any other value is refused at invoke with 422. execution_config used to be accepted and ignored.
  • If a run now fails its audit, give the failing step the data its prompt talks about. A step told to fix or report on data it never receives invents it.

Spending caps on your account (from API version 1.28.0)

  • Taiso can now set spending caps on your account as a whole, across all your keys and your sessions on this site: tokens a day, spend a day and spend a month. No account has one unless Taiso sets it.
  • A request or model call over an account cap gets 429. The message says it is a limit Taiso set on your account, not a problem with your key, which cap was reached and when it resets (00:00 UTC, or the 1st of the month). You cannot see your account's caps yet; ask support. A call must pass both its key's limits and its account's.
  • While an account is over a cap, its scheduled agent runs are skipped when they fire. The schedule stays on and runs again once the cap resets.

Knowledge packs: smart mode (from API version 1.27.0)

  • The model calls a "mode": "smart" search makes (POST /api/v2/tools/knowledge_pack, up to 8 at the default) are now in your usage and count toward your spending limits. They were not recorded before, so smart searches now show a cost.
  • If a limit is reached during a smart search, the search stops and returns what the plain search found, with verification.stop_reason set to "reader_budget". That result is incomplete; if correctness matters more than getting an answer, treat it as a refusal.
  • New distinct: true returns one result per claim instead of one per citation, keeping each claim's best-ranked citation. Every result now carries corroborations and corroborating_sources (1 and [] unless duplicates were collapsed into it). With distinct, max_nuggets counts claims, so a large value can come back short.

SOP block prompts are instructions (from API version 1.26.2)

  • A SOP block's prompt is now carried out as an instruction. Before, it was treated as text to extract data from, so a block asked to write a report could return its own instructions as the report. A prompt phrased as extraction (“Extract the names from…”) works as before. Agents need no recompiling.
  • POST /api/v2/structured-blocks/generate takes mode: "extract" (the default, as before: data is source text) or "instruct" (data is instructions to carry out, and the result comes back in your schema). Any other value is 422.
  • A block's audit can take up to 120 seconds (it was 60), so fewer audits are recorded as unavailable.

404 if you cannot see it, 403 if you cannot change it

Every project, file, chat, job, SOP and agent now follows one rule:

  • It does not exist, or you have no access to it: 404, “… not found or you do not have access”. Before, some endpoints answered 403 here, which confirmed the id was real.
  • You can see it, but your role is too low for what you asked: 403, naming the role required. Ask the owner to share it with you as an editor.

If your client treated 403 as “no access”, treat 404 the same way.

Runs belong to their project

  • A SOP run, agent run or agent schedule is visible to anyone with a role on its project, as jobs already were. Before, agent runs were visible to anyone who could see the agent (for a public agent, everyone), and SOP runs only to whoever started them. Teammates now see each other's runs; a stranger gets 404.
  • Cancelling a run (DELETE /api/v2/sops/runs/{run_id}, DELETE /api/v2/agents/runs/{run_id}) needs editor on its project, like cancelling a job. Cancelling an agent run used to answer 500; cancelling a SOP run now also cancels its job.
  • A job of type agent_execution can only start one of your own, not-yet-started agent runs in the same project; anything else is 404 (or 409 if the run already started).
  • project_id on POST /api/v2/sops/{sop_id}/validate and on POST /api/v2/llm/chat needs a role on that project; otherwise 404.

Writing into a project needs the editor role

A viewer can read a project; changing anything in it needs editor. These calls accepted a viewer before and now answer 403:

  • Invoking a SOP or an agent into a project (POST /api/v2/sops/{sop_id}/invoke, POST /api/v2/agents/{agent_id}/invoke).
  • Creating or updating an agent schedule that writes into a project (POST /api/v2/agents/{agent_id}/schedules).
  • Converting a project file to PDF.
  • Deleting a job, even one you created.
  • Processing a file for search (POST /api/v2/files/{file_id}/process).
  • Recompiling an agent, which now needs editor on the agent.

Reading a SOP's or agent's usage statistics now needs at least viewer access to it.

Schedules pause when their creator loses access

Each time a schedule fires, it checks that its creator can still edit the target project. If not, the schedule is switched off instead of running, and its response says why in paused_reason, with the time in paused_at. Turning it back on clears both.

A schedule's runs belong to whoever created it or last changed it; changing one needs editor on its project and makes you that user. A schedule also pauses when its user can no longer run the agent.

Schedule responses now return the inputs you set in input_config; before, it was always empty. Jobs started by a schedule have ids like every other job (job-1a2b3c…).

Scheduled runs and shared agents now run

  • Every scheduled agent run used to fail with “This run has no API credential”. Each firing now gets a key of its own, for the schedule's user: it can spend at most $1.00, expires after two hours and is revoked when the run ends. It appears in your key list as Scheduled run arun-….
  • A run of an agent shared with you (as executor or editor) used to fail with “SOP not found” or “permission denied”. It now runs, as you.

Authentication

  • A request without a key gets a 401 that says how to get one, with a WWW-Authenticate: Bearer header. The message still starts “Missing Authorization header.”
  • A token that is neither an API key nor a session token is a 401. It could be a 503 before.
  • bearer is accepted as well as Bearer.
  • The OpenAPI document declares a Bearer security scheme and the server URL, so the interactive docs have an Authorize button. Some operations no longer list Authorization as a header parameter. If you generate a client from the spec, regenerate it: the key now comes from the client's auth setting.

Errors

  • An unexpected server error is still a 500 with a request id, but the connection stays open. Before, the next request on the same connection could fail with “Server disconnected”.
  • When file storage is unavailable, file calls answer 503 instead of 500. Retry later.
  • Fetching the result of a job you cannot access is a 404, not a 500. Deleting an agent that does not exist is a 404, not a 403.
  • Sharing an agent with a user who does not exist is a 400, like sharing a SOP, not a 500.

A pinned SOP version is the one that runs

Invoking a SOP with sop_ref: "sop:<sop_id>:<version> (or version) was accepted but ran the SOP's current version. It now runs the version you pinned, and GET /api/v2/sops/runs/{run_id} reports it as sop_version. If you pinned versions before, check runs that depended on an older definition.

Schedules, jobs and webhooks

  • A schedule's timezone is now applied. It was stored but ignored, so every schedule ran on UTC. A schedule with "0 9 * * *" and America/New_York now fires at 9 a.m. New York time. An unknown timezone is refused.
  • An http_call job's endpoint must be a path of this API under /api/v2/; anything else is 400.
  • A webhook_url must be a public address; local and private addresses are refused.
  • Agent steps of type: sop and type: agent are refused when an agent is created: they never actually ran a nested SOP or agent. Chain runs yourself instead.
  • Idempotency-Key values are your own: one another user has used no longer fails your request.

API keys and uploads

  • A key with spending or token limits, an expiry, or one made for a scheduled run cannot create keys through POST /api/v2/users/me/api-keys (403). A name must be 1 to 100 characters. Scopes may be narrowed but not set to * or an admin scope (422); they are recorded and not yet enforced.
  • Knowledge-pack downloads are counted per account (10 a month across all your keys), not per key.
  • Spending limits are checked before every model call, not only when a request starts: a run stops at the call that would go past a limit (429). max_tokens on a chat request is at most 65,536. A streamed call cut short is still recorded, with its usage estimated.
  • Your email address is changed at taiso.ai, where it is verified; PATCH /api/v2/users/me refuses it with 403.
  • Deleting an agent while one of its runs is in progress answers 409.
  • A schedule fires once at each local time, including when daylight saving ends (it fired twice in the repeated hour).
  • A request body over 100 MB, uploads included, is refused with 413, as is a file larger than your remaining storage.
  • A 500 no longer includes the underlying error text. Quote its request_id when you contact support.

MCP server

  • /mcp/rpc now speaks MCP: initialize, ping and notifications are answered, so MCP clients such as Claude Code can connect. Before, every client failed at initialize with HTTP 400.
  • Errors are JSON-RPC errors (-32601 unknown method, -32602 unknown tool or bad arguments, -32002 unknown resource) instead of HTTP 400 and 404. Tool arguments are checked against each tool's published schema.
  • generate_code takes task and language (python or bash), and its snippets run as written. get_agent_template offers research, document_analysis, scheduled_report, multi_agent and content_pipeline; each template is valid agent YAML, which none of the old ones was.
  • /mcp/sse is retired and answers 410. It never handled MCP messages.
  • generate_code's task is one of its listed tasks exactly. Text arguments have length limits (maxLength in each schema). A browser request from an origin the API does not allow is 403, and an unsupported MCP-Protocol-Version header is 400.

Documentation: the id format

Ids are a short prefix, a dash, then hex: prj-1a2b3c4d5e6f, sop-7b19ee0d. Some examples showed prj_abc123, which the API never issued. If your client validates ids against that pattern, accept the dash. A pinned SOP reference is sop:<sop_id>:<version>.

Assist

The step-by-step answers from POST /api/v2/assist/ask now name only endpoints, job types and request bodies the API accepts. Some answers used to suggest calls that failed; if you copied one, compare it with the answer you get now.

New to the API? Start with Getting Started.

Need help?

This page is a starting point. For anything it does not cover, reach out.