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:
{"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 |
| 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 |
| 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 |
| 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 anidcolumn. - Wait first: every 429 carries
Retry-Afterin 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.