Skip to main content
Memory is built from events. Everything that happens with a customer, on any channel or system, enters Niadra as an immutable event: what they said, what a system recorded, what an agent did. Nothing is edited afterwards. A correction is a new event, and erasure is a separate operation that issues a receipt.

Three kinds of event

The kind field says what the event records: Each event carries its channel (whatsapp, voice, app, email, erp, crm, ticket), who spoke in speaker (customer, ai_agent, human_agent or system), the moment in occurred_at, and at least one handle, subject or object saying whom or what it is about. Free text from inside a system, such as a ticket description or an e-mail body, comes in as a message on its own channel. Memory is ordered by occurred_at, never by arrival. A WhatsApp resync that replays old messages lands in the right place. A timestamp outside the clock tolerance is corrected to the arrival time and flagged.

The batch

Every event comes in through POST /v1/batch. A batch takes up to 500 items, with at most 1 MB per item and 2.5 MB per batch, and mixes items of several kinds through the type field: The raw event is stored before the answer goes out. You rarely build a batch by hand: the SDKs keep events in a local queue and send them in batches without blocking the agent.

Idempotency

Every item carries an idempotency_key. Use the provider message id whenever there is one: channel providers resend webhooks for days, and the same id never becomes two events. When there is no id, the SDK mints a UUIDv7, which also sorts by time. Deduplication is by id, never by content similarity. Two identical short messages (“ok”, “yes”) are two messages. A repeated item is counted in duplicates, without an error.

One bad item never fails the batch

The batch answers 200 when every item went in and 207 when any item was rejected, always with a result per item. What went in counts as accepted, what already existed counts as duplicates, and every rejected item shows up in errors with its position and code:
Validation happens in two steps: the edge checks only the shape of the envelope, and each item is checked afterwards. The shape rules, which the SDKs also apply before sending:
  • a message needs text, a transcript or a media reference;
  • a system event needs a canonical_type, such as invoice.credited;
  • an action needs the action block, and the block is only valid when kind is action;
  • every event needs at least one handle, subject or object.
An item over 1 MB comes back with the code too_large. A body over 2.5 MB, or a batch with more than 500 items, is refused whole with 422 invalid_input. The SDK retries with backoff on network failures, 429 and 503, and never retries a validation 4xx.

Conversations, tasks and sessions

The conversation_id is yours: it can be a WhatsApp thread that lasts months, or a call id. Niadra splits each conversation into sessions, windows of activity that close after channel inactivity (about 20 minutes on WhatsApp, 30 in an app) or on conversation.ended. For voice, send conversation.ended when the call hangs up. A call often has two ids, the platform one and the trunk one: send the second in conversation_aliases. Internal agents use a task_id instead of a conversation. A task closes on task.ended or after 10 minutes without activity. The SDKs handle this with conversation() and task(), which emit the end when the block finishes. A new message is readable in the hot tier in under 1 second, already in the live part of the next context read from another channel. Actions and system events reach the recompiled context in under 10 seconds.

Media and late data

Media never travels inside the event. Reserve an upload with POST /v1/media/uploads, giving content_type, size_bytes (up to 500 MB), the sha256 of the bytes and, whenever you know it, the subject the file belongs to: the file is then stored under that person, and erasing them erases it even if no event ever points to it. The answer carries upload_url, upload_headers and expires_at. PUT the bytes to upload_url with exactly the headers of upload_headers: the store refuses any other bytes. Then send the event with content.media_ref and content.media_sha256. The SDKs do the three steps in one call.
Data that arrives later, such as the full transcript of a call, is a new event, not an update. It marks the conversation, and the derived memory is rebuilt with a new version.

Source coverage

The SDK sends a periodic heartbeat with how many events it sent. Niadra compares that with what it received and marks each source ok or silent. The context reports this in coverage, and a silent source can fire a trigger. GET /v1/sources/coverage shows each source day by day, for up to 90 days: what the SDK says it sent, what was received, what was rejected and what arrived with a type your mapping does not cover (unmapped).

Which context the agent used

When an agent answers, the SDK stamps its turn with context_stamp: the etag of the context that went into the prompt and injected_at, the moment it went in. That is how context use tells a context that arrived late from one that arrived and went unused. Inside a conversation() the SDKs stamp it for you when you call mark_injected() in Python or markInjected() in TypeScript; send it yourself only when you build events by hand.

Files: seed identity or backfill history

For volume that does not belong in the conversation path, POST /v1/ingest/files takes a whole file of up to 512 MB, with the track scope, and answers 202 with an import_id:
  • text/csv seeds identity. The header names handle types (phone_e164, email, system_id…), with <type>_scope for the namespace, <type>_2 to <type>_9 for more values of the same type, and the optional idempotency_key, occurred_at and subject_kind. Each row becomes one identify with the method system_import, under the same protections as any other.
  • application/x-ndjson backfills history: each line is one item of POST /v1/batch, validated the same way.
Follow the import with GET /v1/ingest/files/{import_id}: status (queued, running, completed, failed), records, accepted, duplicates, rejected and the first 100 row errors, which carry the row number and never the values.

Corrections

A correction is an event too. POST /v1/feedback takes one action, and each action needs its own fields: retract_fact needs fact_id; correct_fact needs fact_id and value; resolve_open_item needs open_item_id; conversation_outcome needs conversation_id and value. A request without them is refused with 422, instead of being stored and skipped. The answer has the shape of the batch answer. Both SDKs have feedback().

Next steps

Identity and verification

how handles become one customer.

Systems, objects and actions

ERP events and actions that close open items.

Send a batch

the full reference for POST /v1/batch.