Skip to content

Documents

Create, read, replace, patch and delete documents; versions and If-Match.

Every configured collection gets the same set of endpoints:

MethodPathBehaviorSuccess
POST/v1/{realm}/{database}/{collection}Create a document201 + Location + ETag
GET/v1/{realm}/{database}/{collection}List documents200
GET/v1/{realm}/{database}/{collection}/{id}Fetch one document200 + ETag
PUT/v1/{realm}/{database}/{collection}/{id}Replace the whole document200 + ETag
PATCH/v1/{realm}/{database}/{collection}/{id}Partial update (JSON Merge Patch)200 + ETag
DELETE/v1/{realm}/{database}/{collection}/{id}Delete the document204
POST/v1/{realm}/{database}/_batchCreate, replace, patch and delete atomically200
  • An unknown realm, database, collection or id returns 404 not_found. PUT and PATCH never create documents: there are no upserts, so create with POST and let the server choose the id.
  • In a realm with auth: enabled, requests are decided by the collection’s access rules, or allowed outright with an API key in Authorization: Bearer ….
  • Request bodies must be a single JSON object. Anything else returns 400 invalid_json.

Document shape

Documents are returned as bare JSON objects:

{
  "id": "d2fjps30h2ipb32i2hg0",
  "_meta": {
    "created_at": "2026-09-25T21:30:00.000Z",
    "updated_at": "2026-09-25T21:45:12.345Z",
    "version": 3
  },
  "name": "Widget",
  "price": 12.5
}
  • id is generated by the server (xid: 20 characters, sortable by creation time). Clients can never choose it.
  • _meta is owned by the server:
    • created_at is set on create and never changes.

    • updated_at is set on every write.

    • Both timestamps are RFC3339 in UTC with millisecond precision.

    • version is 1 on create and increases by one on every write.

    • In realms with auth: enabled, three ownership fields are added:

      • owner is the id of the signed-in user who created the document (or the user an API key acted for), or null when an API key created it on its own. It never changes.
      • created_by is who created it, and never changes: user:<id>, key:<name>, or anonymous when a create rule lets callers without credentials write.
      • updated_by is who made the last write, in the same form.

      Realms with auth: disabled don’t have them. Documents stored before these fields existed don’t gain owner or created_by; their next write adds updated_by.

  • Single-document responses carry ETag: "<version>", for example ETag: "3".
  • Documents written before versioning existed have no version. They get version: 1 on their next write.
  • id, _id and _meta in request bodies are always ignored.

Create

POST /v1/shop/orders/items
Content-Type: application/json

{ "name": "Widget", "price": 12.5, "status": "active" }

The body is validated against the collection’s schema.json. The response is 201 Created with the stored document and a Location header pointing at it.

Safe retries with Idempotency-Key

A create whose answer is lost (a timeout, a dropped connection) can’t be retried blindly: it may have worked, and the retry would make a second document. Send an Idempotency-Key and retry with the same one:

POST /v1/shop/orders/items
Idempotency-Key: order-42

It is the mechanism of function calls, applied to creates and to batches:

  • The first request with a key runs, and its answer is remembered for 24 hours. The same key with the same body returns that answer again, 201 with the document as it was created (even if it was changed or deleted since), with Idempotent-Replayed: true. Two bodies with the same content, in any key order or spacing, are the same body.
  • The same key with another body answers 422 idempotency_key_reused. The same key while the first request is still running answers 409 request_in_progress: wait and retry.
  • Only a success is remembered. A request that fails (invalid body, refused by the rules, a unique conflict) created nothing, so the key stays free for the corrected retry.
  • A key is private to the collection and the caller: another user, or an API key, can use the same string without meeting it. 1–200 characters of letters, digits, ., _, : and -; anything else is 400.
  • It needs a signed-in user or an API key (anonymous callers all look alike, so they could read each other’s answers) and a realm with authentication enabled (it is where keys are kept): otherwise 400, rather than ignoring a promise of safety.
  • If backd stores the document but fails to store the answer, the key stays “running” for its 24 hours: a retry is told 409 rather than creating a duplicate.

Validation errors

A document that fails its schema returns 400 validation_error with one detail per failing field, using dot paths for nested fields:

{
  "error": {
    "code": "validation_error",
    "message": "document failed schema validation",
    "details": [
      { "path": "address.city", "reason": "got number, want string" },
      { "path": "age", "reason": "minimum: got -1, want 0" },
      { "path": "extra", "reason": "is not allowed" },
      { "path": "name", "reason": "is required" }
    ],
    "request_id": "d2fk1a30h2ipb32i2hh0"
  }
}

Concurrency and If-Match

Every write is conditional on the version it read, so concurrent writers never silently overwrite each other.

If-Match

  • PUT, PATCH and DELETE accept an optional If-Match header with the version you expect, as returned in ETag:

    PATCH /v1/shop/orders/items/d2fjps30h2ipb32i2hg0
    Content-Type: application/merge-patch+json
    If-Match: "3"
    
    { "price": 14 }
    
  • If the document’s current version differs, the response is 412 version_mismatch, with the current version in ETag. This also happens when another write lands between the read and the write.

  • If-Match can list several versions ("3", "4") or be *, which means “any existing document”.

  • Weak tags (W/"3") never match. Malformed values return 400 invalid_header.

Without If-Match

  • PUT and PATCH retry automatically when another write lands between their read and write: re-read, re-merge (PATCH), re-validate, write. They try up to 3 times, with a short random backoff.
  • If every attempt loses the race, the response is 409 write_conflict, and nothing from the request was applied. Clients may retry.
  • A DELETE without If-Match deletes whatever version is current. When access rules apply, the delete is conditional on the version the rule was checked against, and retries like PUT and PATCH: losing every attempt answers 409 write_conflict.

Unique conflicts

A create, replace or update that would repeat a value covered by a unique index (see indexes.json) returns 409 conflict. The details name the index’s fields:

{
  "error": {
    "code": "conflict",
    "message": "a document with the same unique value already exists",
    "details": [ { "path": "email", "reason": "must be unique" } ],
    "request_id": "darf9mq5mh5g00aq9df0"
  }
}

For a compound unique index, each field is listed with the reason must be unique in combination with a, b.

List

GET /v1/shop/orders/items?limit=10&skip=20
{ "items": [ ... ], "limit": 10, "skip": 20, "has_more": true }

By default documents are ordered by id, which is creation order. The endpoint also supports filtering (where), sorting (order_by), counting (count) and paging with a cursor (after, from the next_cursor of the previous page). See Querying.

Replace (PUT)

The body becomes the complete new document and is validated as a whole. Fields that aren’t in the body are removed. id and _meta.created_at are kept.

Update (PATCH)

The body is a JSON Merge Patch that is merged into the stored document:

  • members in the patch overwrite stored values;
  • nested objects are merged recursively;
  • null removes a field.

The merged document is then validated against the schema.

PATCH /v1/shop/orders/items/d2fjps30h2ipb32i2hg0
Content-Type: application/merge-patch+json

{ "price": 14, "discount": null }

Delete

Deletes are permanent unless the collection soft-deletes. The response is 204 No Content, or 404 if the document doesn’t exist (or, in a collection that soft-deletes, is already deleted). If-Match works as for any write.

Batch writes

A batch accepts an Idempotency-Key too: a retry returns the original results.

POST /v1/{realm}/{database}/_batch creates, replaces, patches and deletes documents across the database’s collections in one MongoDB transaction: either every operation applies, or none does.

POST /v1/shop/orders/_batch
Content-Type: application/json

{
  "operations": [
    { "op": "create", "collection": "orders", "document": { "item": "widget", "qty": 2 } },
    { "op": "patch", "collection": "stock", "id": "d2fjps30h2ipb32i2hg0", "patch": { "qty": 8 }, "if_match": "\"3\"" }
  ]
}
{
  "results": [
    { "id": "o7", "item": "widget", "qty": 2, "_meta": { "version": 1, ... } },
    { "id": "d2fjps30h2ipb32i2hg0", "qty": 8, "_meta": { "version": 4, ... } }
  ]
}
  • Each operation is { "op": "create" | "replace" | "patch" | "delete", "collection": "...", ... }. create and replace need document; patch needs patch (a JSON Merge Patch, same rules as PATCH); replace, patch and delete need id. Any of them accepts if_match, exactly like the If-Match header (a version such as "3", or "*" for any existing document).

  • results has one entry per operation, in the same order: the written document for create, replace and patch; just { "id": ... } for delete.

  • Up to 100 operations per batch.

  • Each operation is validated against its collection’s schema.json and, in realms with auth: enabled, checked against the collection’s create/update/delete access rule — exactly like a single write. API keys bypass rules, as they always do.

  • The first operation that fails aborts the whole batch: nothing from any operation is applied. The response is the usual error envelope, with the failing operation’s position (0-based) as the path in details:

    {
      "error": {
        "code": "version_mismatch",
        "message": "operation 1: If-Match does not match the current version \"4\"",
        "details": [ { "path": "1", "reason": "If-Match does not match the current version \"4\"" } ],
        "request_id": "darf9mq5mh5g00aq9df0"
      }
    }
    

    A schema validation failure inside an operation prefixes the field path too, e.g. "1.qty".

  • On a version mismatch a batch answers 412; see the note below.

  • The JS client: client.db('orders').batch([{ op: 'create', collection: 'orders', document: {...} }, { op: 'patch', collection: 'stock', id, patch: {...}, ifMatch }]).

Soft delete

A collection with soft_delete keeps deleted documents recoverable. DELETE answers the same 204, but the document stays, marked in _meta:

"_meta": { "version": 2, "deleted_at": "2026-10-04T09:00:00Z", "deleted_by": "user:d2fjps30h2ipb32i2hg0", "purge_at": "2026-11-03T09:00:00Z" }
  • Everywhere else it is gone: get, list, count, PUT, PATCH, DELETE and batches answer as if it didn’t exist, and its unique values are free again.
  • The trash is read with deleted=only (just the deleted ones) or deleted=include (both) on GET /{collection} and GET /{collection}/{id}. It needs the read rule and the restore rule to allow the document, so who may restore decides who may look. Anything else for deleted, or the parameter on a collection that doesn’t soft-delete, is 400 invalid_query. The marks are queryable: where={"_meta.deleted_at":{"$gte":"2026-10-01T00:00:00Z"}}.
  • POST /{collection}/{id}/restore brings a deleted document back, as it was, at the next version (200 with the document). It needs the restore rule, honours If-Match, and re-checks the document against the schema as it is now (400 if it no longer fits) and its unique values (409 if a live document took them meanwhile). A document that isn’t in the trash is 404.
  • DELETE /{collection}/{id}?purge=true removes a document for good, deleted or not, under the purge rule; the caller must also see it (the read rule, and for a deleted one the restore rule). A collection without a retention keeps deleted documents until someone purges them.
  • Callers outside the rules, API keys, see and restore everything; the rules decide for everyone else, and without a restore or purge rule nobody else may.

Fixing data as an administrator

The admin API serves these same operations past the collections’ rules, at /v1/{realm}/_admin/data/{database}/{collection}, for administrators who hold the data area. Writes there are audited and documents they create have no owner.

Schema changes

After schema.json changes, existing documents are returned as they are, even if they no longer match. Every write must produce a document that is valid against the current schema.

Edit this page · Last updated 08 Oct 2026