Errors & Rate Limits
Error envelope
Errors are returned as a JSON body alongside a non-2xx HTTP status code:
{
"exception": "frappe.exceptions.ValidationError",
"exc_type": "ValidationError",
"_server_messages": "[\"{\\\"message\\\": \\\"Priority is a required field\\\", \\\"indicator\\\": \\\"red\\\"}\"]"
}
_server_messages is a JSON-encoded array of individual, user-facing
messages — parse it if you want to surface field-level validation errors to
your own users.
HTTP status codes
| Status | Meaning |
|---|---|
200 OK | Request succeeded. |
202 Accepted | Delete succeeded. |
400 Bad Request | Malformed request (e.g. invalid JSON in filters). |
401 Unauthorized | Missing or invalid credentials. |
403 Forbidden | Authenticated, but the user lacks permission for this doctype/record, or the method isn't whitelisted. |
404 Not Found | Doctype or document does not exist. |
409 Conflict | Document was modified by someone else since you last fetched it (timestamp mismatch). |
417 Expectation Failed | A whitelisted method raised an application-level exception. |
429 Too Many Requests | Rate limit exceeded — see below. |
5xx | Server-side error. Safe to retry with backoff. |
Conflict handling (optimistic locking)
Every document carries a modified timestamp. If you fetch a document,
someone else updates it, and you then PUT your (now stale) copy, the
request returns 409 Conflict. Re-fetch the document, re-apply your
change, and retry.
Rate limits
API requests are rate-limited per API key to protect the shared infrastructure. Current limits are returned on every response via headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp when the window resets. |
When you exceed the limit, you'll receive 429 Too Many Requests with a
Retry-After header (seconds to wait before retrying). Implement
exponential backoff for 429 and 5xx responses rather than retrying
immediately.
Idempotency
GET and DELETE requests are naturally idempotent. For POST (create),
there is no built-in idempotency key — if you need to safely retry a create
request, check for an existing record with your own unique business key
before creating a new one, or design your integration to be reconciliation-
based rather than relying on exactly-once creation.