Three kinds of event
Thekind 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 throughPOST /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 anidempotency_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 asaccepted, what already existed counts as duplicates, and every rejected item shows up in errors with its position and code:
- a message needs text, a transcript or a media reference;
- a system event needs a
canonical_type, such asinvoice.credited; - an action needs the
actionblock, and the block is only valid whenkindisaction; - every event needs at least one handle, subject or object.
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
Theconversation_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 withPOST /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.
Source coverage
The SDK sends a periodicheartbeat 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 withcontext_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/csvseeds identity. The header names handle types (phone_e164,email,system_id…), with<type>_scopefor the namespace,<type>_2to<type>_9for more values of the same type, and the optionalidempotency_key,occurred_atandsubject_kind. Each row becomes oneidentifywith the methodsystem_import, under the same protections as any other.application/x-ndjsonbackfills history: each line is one item ofPOST /v1/batch, validated the same way.
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.
