# Errors and limits

> Response codes of the Ghosty Studio API, what each one means and how to retry; sizes and caps.

URL: https://www.ghosty.studio/en/docs/api/errors-and-limits

## Codes

| Code | Body | What to do |
|---|---|---|
| `202` | `{ turnId, key, estado, enCola, reemplazoAnterior, repetido, inyectado }` | The turn was **accepted**, not finished. Listen to the [events](/en/docs/api/events). `enCola` (queued) > 0 = there are turns ahead. `repetido` (repeated) = you had already sent that `turnId`, it was not duplicated. `estado` = state; `reemplazoAnterior` = replaced the previous one; `inyectado` (injected) = your message **joined the turn already running**: no new turn, keep listening to the same `turnId`. |
| `400` | `{ error }` | Invalid body: missing `content`, `id`/`optionId`, empty model… Don't retry without changing it. |
| `401` | text | No token or expired. Refresh and retry **once**. |
| `402` | `{ error: "quota_exhausted" \| "trial_expired" \| "own_key_required", message }` | Allowance exhausted, trial expired or a model that requires your own key. The `message` is for display. |
| `403` | text | A scope is missing. Don't retry: request the scope at the next login. |
| `404` | text or `{ error: "not_found" }` | The agent isn't yours, or the resource doesn't exist. Someone else's agent responds 404 and not 403, on purpose. |
| `405` | `{ error: "method_not_allowed" }` | Wrong method. |
| `409` | `{ error: "agente_no_acp", motor }` | The agent doesn't speak this API's conversation protocol (engine without ACP). `motor` = engine. |
| `413` | `{ error: "audio demasiado grande" }` | Audio or file above the cap ("audio too large"). |
| `503` | `{ error: "reiniciando" }` + `Retry-After: 10` | Deploy in progress ("restarting"). Retry with the **same `turnId`** after the `Retry-After`: it doesn't duplicate. |

## Idempotency

`POST …/messages` accepts a `turnId` of yours (UUID). If you repeat the request with the same `turnId` — because of a timeout, a 503, a bad network — the server replies `repetido: true` and doesn't create a second turn. Always use it.

A correct retry honors `Retry-After` and reuses the `turnId`:

```typescript
async function sendTurn(url: string, body: { content: string; turnId: string }, token: string) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(url, {
      method: "POST",
      headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
      body: JSON.stringify(body), // same turnId each attempt: the server does not duplicate
    });
    if (res.status !== 503) return res;
    const wait = Number(res.headers.get("Retry-After") ?? 10) * 1000;
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error("the platform kept restarting after 3 attempts");
}
```

## Concurrency

An agent runs **one turn at a time per conversation**. A new message in a conversation with a turn in progress **joins that turn** (`inyectado: true`): the agent reads it as soon as the tool it is running finishes and answers in the same reply, keeping the work done so far. It is the way to correct it mid-work. If the turn was already closing and the message could not get in, the previous one is **stopped and replaced** (`reemplazoAnterior: true`). Only whoever requested the turn can replace it. Different conversations of the same agent run in parallel up to the plan's cap; those waiting for a machine count in `enCola`.

## Sizes

| What | Cap |
|---|---|
| Attachments per message | 8 |
| Audio for `POST /api/v2/me/stt` | per plan; exceeding it returns `413` |
| Signed URL of a file | 6 hours; request it again with `GET /api/v2/me/files/:id` |
| OAuth2 authorization code | 60 s |
| Access token | 1 h |
| Refresh token | 90 days, rotates on every use |
| Unanswered permission | 10 min → denied |

## Rate

There is no published requests-per-second limit in beta; the real cap is the plan's token allowance and the machines awake at once. If you see `429`, honor `Retry-After`.
