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-Key header, or
  • The event_id field in the request body (event_id takes 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 202 ticket 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).