# Errors, rate limits and idempotency

> FoodPerfect MCP error codes, what to retry, rate limits, and how Idempotency-Key makes retries safe.

## Error format

Every error has the same envelope:

```json title="Error envelope"
{
  "error": {
    "code": "rate_limited",
    "message": "Human-readable explanation.",
    "requestId": "7f3a…",
    "retryable": true,
    "details": { }
  }
}
```

It arrives in one of two places:

- **Tool errors** come back as a normal tool result with `isError: true`, and the envelope in `structuredContent` (and as JSON text in `content`). Most MCP clients show these to the model so it can react.
- **Request errors**, such as a bad key, a disallowed origin or the rate limit, are returned as an HTTP status (`401`, `403`, `429`) with the envelope as the JSON body, before any tool runs.

Every response has an `x-request-id` header. Include it when you contact support.

## Error codes

| Code | Retry? | Meaning and what to do |
|---|---|---|
| `invalid_input` | No | Arguments don't match the schema, or an idempotency key was reused with different arguments. Fix the call. |
| `unauthorized` | No | Key missing, wrong, revoked or expired; the key's scopes don't cover the tool; or the paid subscription isn't active. |
| `not_found` | No | The id doesn't exist or belongs to another account. |
| `payment_required` | No | The account has no active plan or trial (canceled, or the last payment failed). Fix billing in the app's Billing page, then call again. |
| `insufficient_credits` | No | The balance can't cover the call. `details` has `availableMicros` and `requiredMicros`. Top up, don't retry. |
| `requires_approval` | No | Not a failure: the action needs confirmation. See [Approvals](/docs/approvals/). |
| `rate_limited` | **Yes** | Too many calls. Wait for `Retry-After` / `details.retryAfter` seconds. |
| `retryable_upstream_failure` | **Yes** | An image, video or AI provider failed or timed out, or the same idempotent call is still running. Retry with backoff. |
| `internal` | No | Something broke on our side. Try once more later, then contact support with the request id. |

Retry only when `retryable` is `true`: exponential backoff starting around 2 seconds, at most 3 attempts, with the **same** `Idempotency-Key`.

## Rate limits

| Limit | Default |
|---|---|
| Tool calls per API key | 60 per minute (`initialize` and `tools/list` are free) |
| `generate_reel_storyboard` | 60 per account per day (UTC) |
| `scrape_trending_hooks`, trending hooks in `resolve_reel_generators` | 10 searches per account per hour |
| Request body | 1 MB |

Over the per-key limit you get HTTP `429`:

```http title="Response"
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{"error":{"code":"rate_limited","retryable":true,"details":{"rateLimit":60,"retryAfter":12}, …}}
```

Higher per-key limits (up to 600 a minute) are available on request.

## Idempotency

A network error can leave you unsure whether a call ran. Idempotency keys make retries safe: send the same key again and you get the first result back instead of a second charge or a duplicate post.

Send the header on any tool that creates, changes or spends (everything marked *Write* in the [tool reference](/docs/tools/)). It's optional, but recommended whenever your code retries:

```http title="Header"
Idempotency-Key: 5f0c2d6e-2a8b-4a61-9d0f-6a1c3e7b9c42
```

(`X-Idempotency-Key` works too.) Read tools ignore it.

| Situation | Result |
|---|---|
| New key | The call runs; its result is stored for **24 hours**. |
| Same key, same tool, same arguments | The stored result is returned. Nothing runs or is charged again. |
| Same key, different arguments | `invalid_input`. Use a new key for a new call. |
| Same key while the first call is still running | `retryable_upstream_failure`. Wait and retry. |
| Same key after the call failed | The failed call was refunded, so the retry runs and is charged like a new call. |
| Same key after the first call never finished (server crash) | Within 30 minutes its charge is refunded automatically, and the retry runs as a new call. |
| No key | The call runs normally, without duplicate protection. |

Generate a fresh random key (a UUID is ideal) for every new action, and reuse it only to retry that action. Keys are scoped to your account and the tool, so they can't collide with anyone else's.
