REST error bodies and request ids, MCP tool errors, and safe build retries.
REST errors are typed
A REST machine caller — API key or connected app — gets a structured error rather than prose to substring-match:
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "insufficient scope: build:write required",
"request_id": "req_2ede35dd-b5ad-4f06-90e9-7859d799f156"
}
}
Branch on type and code, never on message — the message is written for people and is free to change. The types are auth_error, permission_error, invalid_request_error, not_found_error, rate_limit_error and api_error.
On a 5xx the message is deliberately generic. Quote the request_id instead — it is on REST responses as the X-Request-Id header and it is what lets us find the one log line that matters.
MCP tool errors
MCP tool refusals, including exhausted budgets, return isError: true with prose in content inside a JSON-RPC HTTP 200 response. They do not use the REST typed error envelope or rate-limit headers. Inspect the tool result, not only the HTTP status. Use whoami for account and scope information; budget errors signal when to wait before retrying.
Starting a build twice by accident
A build that times out on your side may well have started on ours, and retrying blindly pays for a second generation. Send an Idempotency-Key of your choosing:
POST /app/api/v3/runs
Idempotency-Key: build-2026-09-08-homepage-01
A retry with the same key returns the first call’s run instead of starting another, and carries Idempotent-Replay: true. Keys are honoured for 24 hours per account. A malformed key is rejected rather than silently ignored — you asked for protection, so not quietly giving it would be worse than refusing. On MCP the same thing is the idempotencyKey argument to build_popup.
Waiting for a build
Prefer watch_job over a polling loop. It waits for the job and emits progress notifications while it does, then returns the terminal status. If it hits its timeout it says so and reports the status so far, so you can call it again.
Use get_job for a single status read or when a client cannot hold a request open. REST clients can stream progress at /app/api/v3/runs/{runId}/events.
Both take a jobId, and the runId build_popup hands you is one — pass it as it is. The same two tools wait on a queued design-thread change, so there is one way to wait for anything this API starts.
One thing worth knowing either way: read terminal, not the status word. A run that was interrupted resumes and reports itself as still running; treating that word as a failure was the most common mistake on this surface, and the boolean is there so you never have to.
Migrating an existing client? watch_build and get_build_status have been removed. Replace them with watch_job and get_job, passing the old runId as jobId. Build errors now report status: failed; sourceStatus preserves the underlying run status. A selected job is terminal for waiting but has not built anything: call select_variantin the design thread to start its build.
Waiting for a design thread
The design stage has the same thing: watch_design_thread. It waits for the thread to NEED you — the agent asks a discovery question, the concept cards and their mockups are all ready, or the thread ends — and returns at once if one of those already holds. A half-drawn board is not one of them, so you are not woken for every mockup that lands. On timeout it says so, and you call it again; never sleep or poll get_design_thread between calls. The reason and next fields say which of the four happened and what to do about it.
Rate limits
REST responses passing the rate-limit middleware carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds). A REST budget refusal is a 429 with Retry-After. Use the remaining allowance to pace requests; concurrent calls can still exhaust it, so handle refusals too.
Machine-readable contract
The REST surface is described by an OpenAPI 3.1 document, and there is a short pointer file for agents:
GET /.well-known/openapi.json
GET /llms.txt
Both are readable without a credential, so a client can decide whether to integrate before anyone creates one.