Write API
All write endpoints live under /api/v1 and require a Bearer token with the write scope.
Authorization: Bearer <api-key>
The tenant is derived from the key. It never appears in the URL.
Authentication errors
| Status | Meaning |
|---|---|
401 Unauthorized |
Missing or invalid API key |
403 Forbidden |
Key lacks the required scope or collection access |
Reliability mode
Every write endpoint accepts an optional X-Letopis-Mode header that overrides the collection’s default mode for this request:
| Value | Response | Guarantee |
|---|---|---|
strict |
201 after MongoDB write |
Synchronous; confirmed before responding |
durable |
202 + ticket |
Queued to Redis Streams; worker writes asynchronously |
fast |
202 + ticket |
In-memory queue; maximum throughput, lower durability |
Absent the header, the collection’s configured reliability_mode applies (default durable).
202 responses return a ticket_id you can poll at GET /tickets/{id}.
Idempotency
To prevent duplicate events on retry, pass a client-generated key via:
- The
Idempotency-Keyheader, or - The
event_idfield in the request body (event_idtakes precedence when both are present).
If the same key arrives again within the dedup window (default 24 h, configurable), the server replays the original response:
200 {"status": "duplicate"}if the original already landed.- The same
202ticket body if the original was accepted but not yet stored.
No second event is written.
Ingest full state
POST /api/v1/collections/{collection}/entities/{entityId}/state
Send the entity’s complete current state. The server computes the diff against the last known state.
Request body
{
"state": {
"title": "Acme Corp",
"amount": 5000,
"stage": "prospect"
},
"op": "update",
"event_id": "evt-001",
"author_id": "user-42",
"source": "crm-backend",
"ts_source": "2026-06-01T10:00:00Z",
"expected_version": 3,
"meta": {
"ip": "10.0.0.1"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
state |
object | Yes | The full current state of the entity |
op |
string | No | create or update; inferred if absent |
event_id |
string | No | Client-assigned ID for idempotency |
author_id |
string | No | Opaque identifier of who made the change |
source |
string | No | Identifier of the source system |
ts_source |
RFC3339 | No | Timestamp of the change in the source system |
expected_version |
integer | No | Optimistic lock: fails with 409 if the entity’s current version differs |
meta |
object | No | Arbitrary metadata stored with the event |
flow |
object | No | Business-flow block |
Responses
| Status | Meaning |
|---|---|
201 Created |
Written to MongoDB (strict mode) |
202 Accepted |
Queued (durable or fast mode); body contains ticket_id |
400 Bad Request |
Invalid request body or collection name |
409 Conflict |
expected_version mismatch |
422 Unprocessable Entity |
State did not produce a diff (no-op) |
Ingest diff
POST /api/v1/collections/{collection}/entities/{entityId}/diff
Send a ready-made diff directly. The server validates the format and stores it as-is.
Request body
{
"diff": [
{"path": "amount", "op": "change", "old": 5000, "new": 7500},
{"path": "stage", "op": "change", "old": "prospect", "new": "qualified"}
],
"author_id": "user-42",
"source": "crm-backend"
}
Each element in diff: {path, op, old, new} where op is add, change, or remove.
Delete entity
POST /api/v1/collections/{collection}/entities/{entityId}/delete
Records a delete event. The entity’s state is cleared; its history (events and snapshots) is retained. A subsequent state ingest will record a new create event.
This is a logical delete. For physical removal (GDPR), use the Admin API purge endpoint.
Batch write
POST /api/v1/collections/{collection}/batch
Write multiple entity changes in a single request. Each item in the writes array follows the same schema as a single state or diff write. Batch writes share the request’s reliability mode.
{
"writes": [
{
"entity_id": "deal-1",
"type": "state",
"state": {"title": "Acme Corp", "amount": 7500}
},
{
"entity_id": "deal-2",
"type": "diff",
"diff": [{"path": "stage", "op": "change", "old": "prospect", "new": "won"}]
}
]
}
The response includes per-item results. Items are processed independently; a failure in one item does not roll back others.
Poll a ticket
GET /api/v1/tickets/{ticketId}
Track an asynchronous write accepted with 202.
Response
{
"ticket_id": "tkt_01hx...",
"status": "stored",
"entity_id": "deal-1",
"collection": "crm.deals",
"version": 4,
"ts_stored": "2026-06-02T09:00:01Z"
}
status transitions: accepted → processing → stored (success) or failed (with error field). Tickets expire after the configured TTL (default 24 h).