# Errors

> Every error SkimmerDB answers with: its shape, each status and code, what to do, and which requests are safe to retry.

Every error has the same shape, whatever the status:

```json
{"error": "password is too short (at least 8)", "code": "invalid", "fields": {"password": "is too short (at least 8)"}}
```

| Field | What |
|---|---|
| `error` | What went wrong, in a sentence |
| `code` | A word to act on (below) |
| `fields` | With invalid input: each field, and what's wrong with it |
| `retry_after` | With `write_busy`: seconds to wait (also the `Retry-After` header) |

Branch on `code`, not on `error`'s wording, which may change. The OAuth endpoints answer with OAuth's own error shape, and `/mcp` with JSON-RPC errors.

## Codes

| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | `bad_request` | The request is malformed: not JSON, a missing body | Fix the request |
| 401 | `unauthorized` | No key, or a bad, revoked or expired one | Send `Authorization: Bearer <key>` with a live key |
| 402 | `payment_required` | The free plan's allowance for this period is used up (the message says which) | Wait for the next period, or upgrade at [skimmerdb.com/pricing](https://skimmerdb.com/pricing) |
| 402 | `namespace_limit` | The plan's namespace count, or the namespaces it may make in a day | Delete namespaces, or upgrade |
| 402 | `email_unverified` | A free tenant whose owner hasn't verified their email is past its reduced allowance | Verify the email (the signup email's link, or a password reset) |
| 403 | `forbidden` | The key's scope or namespaces don't allow this, or the tenant is suspended | Use a key with the right [scope](/guides/keys) |
| 404 | `not_found` | No such namespace, document or key, or one this key can't see | Check the name; a write makes a namespace, a delete doesn't |
| 409 | `conflict` | It exists already (a namespace name), or a linked namespace refused a write | Use another name; linked namespaces are read-only |
| 413 | `too_large` | The body, a document or a query's reads are over a limit | Split it; see [Limits](/reference/limits) |
| 422 | `invalid` | The input is understood but wrong: the message (and `fields`) says why | Fix the input |
| 422 | `query_timeout` | A count or SQL query ran past the plan's time limit | Narrow it (a filter, a smaller window) |
| 422 | `scan_refused` | A regex the index can't narrow, on a big namespace | Add a literal to the pattern, or send `"scan": true` |
| 429 | `rate_limited` | Over the tenant's or the key's requests a second, requests in flight, SQL queries at once, or a key's cap | Wait `Retry-After` seconds and retry |
| 429 | `write_busy` | Writes are arriving faster than the namespace commits them. Nothing was written | Wait `retry_after` seconds and resend the same batch |
| 500 | `internal` | A bug on our side | Retry; tell us if it persists |
| 503 | `unavailable` | Object storage failed. Nothing was written | Retry |
| 503 | `write_unknown` | The write may or may not have been applied | Retry it: writes are by id, so a retry is safe |
| 422 | `embedding_key_missing` | The namespace embeds with a provider this tenant has no key for | Store one (`PUT /v1/embedding-keys/{provider}` or the dashboard), or send vectors |
| 422 | `embedding_key_invalid` | The provider refused the tenant's key | Store a working key |
| 422 | `embedding_rejected` | The provider refused the request (an unknown model, say) | Check the model |
| 409 | `embedding_dims` | The model can't make vectors of the namespace's length | Another model, or a new namespace |
| 429 | `embedding_rate_limited` | The provider is rate-limiting the key; `retry_after` says when | Wait, then retry: nothing was written |
| 502 | `embedding_failed` | The embedding provider failed | Retry: nothing was written, and writes are by id |
| 507 | `quota_full` | The tenant's storage quota is full | Delete documents or namespaces, or upgrade |

A search that hits its time limit doesn't fail: it answers what it found with `"partial": true` and `"timed_out": true`. A search whose object-storage reads failed answers `"partial": true`. Treat partial answers as incomplete, never as "nothing else matched".

## Retrying

- **Safe to retry as is:** every read; writes, updates and deletes by id (`POST .../docs`, `PATCH .../docs`, `POST .../delete`) after a 429, a 503 or a timeout, since writing the same ids again gives the same result; imports with an `id` column.
- **Wait first:** every 429 carries `Retry-After` in seconds. Back off for at least that long.
- **Don't retry unchanged:** 400, 401, 403, 404, 409, 413 and 422 (except `query_timeout`, which may pass on a warmer namespace or a narrower query).
- **Creating a namespace** (`POST /v1/namespaces`) answers 409 if a retry finds it made already: treat that as success.

The Python and TypeScript clients retry 429 and 503 for you, honouring `Retry-After`.

## Rate limit headers

Answers that count against a rate (namespaces, documents, search, count, SQL, import, MCP) carry:

| Header | Meaning |
|---|---|
| `RateLimit-Limit` | Requests allowed in the window: the tighter of the tenant's plan and the key's own `rate_per_s` |
| `RateLimit-Remaining` | Requests left in the window |
| `RateLimit-Reset` | Seconds until the window resets |
| `RateLimit-Policy` | The limit and its window, e.g. `100;w=1` (one-second windows) |
| `Retry-After` | On a 429: seconds to wait |

These follow the IETF RateLimit headers draft (07). Limits are per server: see [Limits and plans](/reference/limits).
