# Errors, retries and limits

Canonical URL: https://support.optimonk.com/en/articles/errors-retries-and-limits

# Errors, retries and limits

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:

```json
{ "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_variant`in 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.
