Documents
Create, read, replace, patch and delete documents; versions and If-Match.
Every configured collection gets the same set of endpoints:
| Method | Path | Behavior | Success |
|---|---|---|---|
POST | /v1/{realm}/{database}/{collection} | Create a document | 201 + Location + ETag |
GET | /v1/{realm}/{database}/{collection} | List documents | 200 |
GET | /v1/{realm}/{database}/{collection}/{id} | Fetch one document | 200 + ETag |
PUT | /v1/{realm}/{database}/{collection}/{id} | Replace the whole document | 200 + ETag |
PATCH | /v1/{realm}/{database}/{collection}/{id} | Partial update (JSON Merge Patch) | 200 + ETag |
DELETE | /v1/{realm}/{database}/{collection}/{id} | Delete the document | 204 |
POST | /v1/{realm}/{database}/_batch | Create, replace, patch and delete atomically | 200 |
- An unknown realm, database, collection or
idreturns404 not_found. PUT and PATCH never create documents: there are no upserts, so create with POST and let the server choose theid. - In a realm with
auth: enabled, requests are decided by the collection’s access rules, or allowed outright with an API key inAuthorization: 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
}
idis generated by the server (xid: 20 characters, sortable by creation time). Clients can never choose it._metais owned by the server:created_atis set on create and never changes.updated_atis set on every write.Both timestamps are RFC3339 in UTC with millisecond precision.
versionis1on create and increases by one on every write.In realms with
auth: enabled, three ownership fields are added:owneris the id of the signed-in user who created the document (or the user an API key acted for), ornullwhen an API key created it on its own. It never changes.created_byis who created it, and never changes:user:<id>,key:<name>, oranonymouswhen a create rule lets callers without credentials write.updated_byis who made the last write, in the same form.
Realms with
auth: disableddon’t have them. Documents stored before these fields existed don’t gainownerorcreated_by; their next write addsupdated_by.
- Single-document responses carry
ETag: "<version>", for exampleETag: "3". - Documents written before versioning existed have no
version. They getversion: 1on their next write. id,_idand_metain 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,
201with the document as it was created (even if it was changed or deleted since), withIdempotent-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 answers409 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 is400. - 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
409rather 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-Matchheader with the version you expect, as returned inETag: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 inETag. This also happens when another write lands between the read and the write.If-Matchcan list several versions ("3", "4") or be*, which means “any existing document”.Weak tags (
W/"3") never match. Malformed values return400 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-Matchdeletes 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 answers409 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;
nullremoves 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": "...", ... }.createandreplaceneeddocument;patchneedspatch(a JSON Merge Patch, same rules as PATCH);replace,patchanddeleteneedid. Any of them acceptsif_match, exactly like theIf-Matchheader (a version such as"3", or"*"for any existing document).resultshas one entry per operation, in the same order: the written document forcreate,replaceandpatch; just{ "id": ... }fordelete.Up to 100 operations per batch.
Each operation is validated against its collection’s
schema.jsonand, in realms withauth: enabled, checked against the collection’screate/update/deleteaccess 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 thepathindetails:{ "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,DELETEand 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) ordeleted=include(both) onGET /{collection}andGET /{collection}/{id}. It needs thereadrule and therestorerule to allow the document, so who may restore decides who may look. Anything else fordeleted, or the parameter on a collection that doesn’t soft-delete, is400 invalid_query. The marks are queryable:where={"_meta.deleted_at":{"$gte":"2026-10-01T00:00:00Z"}}. POST /{collection}/{id}/restorebrings a deleted document back, as it was, at the next version (200with the document). It needs therestorerule, honoursIf-Match, and re-checks the document against the schema as it is now (400if it no longer fits) and its unique values (409if a live document took them meanwhile). A document that isn’t in the trash is404.DELETE /{collection}/{id}?purge=trueremoves a document for good, deleted or not, under thepurgerule; the caller must also see it (thereadrule, and for a deleted one therestorerule). 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
restoreorpurgerule 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.