Skip to content

HTTP API

Request IDs, request bodies, errors and health endpoints.

OpenAPI specification

The whole HTTP API is described in api/openapi.yaml (OpenAPI 3.1): every endpoint, status, response body and header. The server is tested against it on every change (see API contract), so it can be used to generate or check clients. Collections’ fields depend on your configuration, so documents appear there as open objects with exactly described id and _meta.

Request IDs

Every request gets a request ID:

  • If the client sends a valid X-Request-ID header, backd uses it: 1–128 characters from A–Z a–z 0–9 . _ : -.
  • Otherwise backd generates one.
  • The ID is always returned in the X-Request-ID response header, included in every log line for the request, and included in error bodies.

Request bodies

  • Bodies larger than MAX_BODY_BYTES (default 1 MiB) are rejected with 413 payload_too_large.
  • POST and PUT require Content-Type: application/json.
  • PATCH requires Content-Type: application/merge-patch+json.
  • The only parameter allowed on these content types is charset=utf-8. Anything else returns 415 unsupported_media_type.

Caching

Responses depend on who is asking, so backd tells caches (browsers, proxies, CDNs) how to keep them apart:

ResponsesHeaders
Everything under /v1/Vary: Origin, whether or not the request has an Origin header, so an answer for one web origin is never served to another
Document routes (/v1/{realm}/{database}/{collection}…)Also Vary: Authorization, so a response cached for an anonymous caller is never served to a signed-in one, or the other way around
Document routes, when the request has an Authorization headerAlso Cache-Control: private, no-cache: shared caches never store the response, and browsers revalidate it before reusing it
/_auth, /_admin, and function calls (_func, _jobs)Cache-Control: no-store: tokens, account data and function results are never cached

Anonymous document responses carry no Cache-Control, so a proxy or CDN may cache public data, for example published posts, by its own rules. The Vary headers make that safe. Errors on these routes carry the same headers.

Errors

Every error uses the same JSON envelope. Success responses are never wrapped.

{
  "error": {
    "code": "validation_error",
    "message": "document failed schema validation",
    "details": [
      { "path": "email", "reason": "must match format \"email\"" }
    ],
    "request_id": "d2fk1a30h2ipb32i2hh0"
  }
}

details is omitted when empty. code is stable and meant for programs to check; message is meant for humans.

HTTPcodeWhen
400invalid_jsonBody is not a single valid JSON object
400validation_errorDocument fails its schema, or an /_auth or /_admin request body is invalid (details per path)
400invalid_headerMalformed If-Match header, or an X-Backd-On-Behalf-Of header that isn’t allowed or names no user of the realm
400invalid_queryBad where, order_by, limit, skip or count, or unknown query parameter
400idempotency_key_requiredA function with idempotency: required was called with no Idempotency-Key
401unauthenticatedMissing, unknown, expired or revoked session token or API key, where credentials are required (see API keys)
401invalid_credentialsWrong email or password
403forbiddenThe operation isn’t allowed: sign-up in a realm where it isn’t open, no access rule allows it, a session on the admin API, or acting on behalf of a disabled user
404not_foundUnknown route, realm, database, collection or id, or a document the caller’s access rules hide
405method_not_allowedMethod not supported on the route
409conflictA unique index is violated (details name the fields)
409write_conflictA PUT, PATCH or conditional DELETE lost the race against concurrent writes 3 times
409email_takenSign-up with an email that is already registered
409request_in_progressA function call with this Idempotency-Key is still running
412version_mismatchIf-Match doesn’t match the current version
403invalid_file_linkA file link that was changed, has expired or whose key was rotated
404file_missingThe document holds a file the storage no longer has
404version_unavailable?version= names an image version that isn’t ready, with its status and reason in details
409too_many_filesA multiple file field is at max_files
409upload_mode_mismatchA proxy upload to a direct field, or a direct start on a proxy field
409file_not_uploadedA direct upload was completed before its file reached the bucket
413payload_too_largeBody exceeds MAX_BODY_BYTES; an upload, max_size or BACKD_MAX_UPLOAD_BYTES
422invalid_imageA JPEG or PNG too damaged to be copied without its metadata
413quota_exceededAn upload would pass the realm’s or the owner’s storage quota
415unsupported_file_typeThe type detected from an upload isn’t one the field accepts
416range_not_satisfiableA Range outside the file
415unsupported_media_typeWrong Content-Type for the method (a webhook function’s body isn’t required to be JSON at all)
422upload_mismatchA direct upload’s size or SHA-256 isn’t the declared one: the object was deleted
422idempotency_key_reusedA function Idempotency-Key was already used with different input
429too_many_requestsToo many failed logins (see brute-force protection); or a function, or its realm, is at its concurrency limit; or the caller is over a function’s rate limit. Wait Retry-After seconds
500function_failedA function threw an error other than ctx.error, crashed, or went over its memory or CPU limit
500internal_errorUnexpected failure. Details are only in the logs
500invalid_outputA function’s output is larger than max_output, or doesn’t match output.schema.json
500secret_missingA function declares a secret that isn’t set for its database or realm
503storage_unavailableA realm’s file storage can’t be reached, or its keys aren’t set
503unavailableNot ready, MongoDB unreachable, MONGO_OP_TIMEOUT exceeded, too many password operations at once, or the functions executor isn’t configured, doesn’t answer, or is at capacity (all with Retry-After where the caller can usefully retry)
504function_timeoutA function didn’t finish within its timeout; it was stopped

A function’s own errors, thrown with ctx.error(status, code, message), carry whatever 4xx status and lower-case code it chose — see Calling a function.

Health endpoints

These live outside the /v1 prefix:

EndpointMeaning
GET /healthzThe process is alive: always 200 {"status":"ok"}
GET /readyzConfig loaded, provisioning done and MongoDB reachable: 200 {"status":"ready","config":"<fingerprint>"}, otherwise 503 unavailable. config is the fingerprint of the config the instance runs (see Deploying config)
Edit this page · Last updated 08 Oct 2026