> ## Documentation Index
> Fetch the complete documentation index at: https://docs.niadra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Eventos e o lote

> Mensagens, eventos de sistema e ações, enviados em lote, idempotentes e com erro por item.

A memória nasce de eventos. Tudo o que acontece com um cliente, em qualquer canal ou sistema, entra na Niadra como um evento imutável: o que ele disse, o que um sistema registrou, o que um agente fez. Nada é editado depois. Correção é evento novo, e apagamento é uma operação à parte, com comprovante.

## Três tipos de evento

O campo `kind` diz o que o evento registra:

| `kind`         | O que é                                                   | Exemplo                                                        |
| -------------- | --------------------------------------------------------- | -------------------------------------------------------------- |
| `message`      | Algo dito ou enviado numa conversa. É o padrão.           | Marina escreve no WhatsApp às 14h02 que o técnico não apareceu |
| `system_event` | Uma mudança num sistema de registro, com `canonical_type` | O ERP emite `invoice.credited` às 14h06                        |
| `action`       | O que um agente fez num sistema, com o bloco `action`     | O agente de cobrança lança o crédito de 40 na fatura 0823      |

Cada evento traz o `channel` (`whatsapp`, `voice`, `app`, `email`, `erp`, `crm`, `ticket`), quem falou em `speaker` (`customer`, `ai_agent`, `human_agent` ou `system`), o momento em `occurred_at` e pelo menos um handle, sujeito ou objeto que diz de quem ou do que ele fala. Texto livre de dentro de um sistema, como a descrição de um ticket ou o corpo de um e-mail, entra como `message` num canal próprio.

A ordem da memória é a de `occurred_at`, nunca a de chegada. Uma ressincronização do WhatsApp que reenvia mensagens antigas cai no lugar certo. Carimbo de relógio fora da tolerância é corrigido pela hora de chegada e marcado.

## O lote

Todo evento entra por `POST /v1/batch`. Um lote aceita até **500 itens**, com até **1 MB por item** e **2,5 MB por lote**, e mistura itens de vários tipos pelo campo `type`:

| `type`               | Para quê                                                |
| -------------------- | ------------------------------------------------------- |
| `event`              | Mensagem, evento de sistema ou ação                     |
| `identify`           | Afirma que dois ou mais handles são do mesmo sujeito    |
| `verify`             | Eleva o nível de verificação de uma conversa ou tarefa  |
| `conversation.ended` | Fecha a sessão agora, sem esperar a inatividade         |
| `task.ended`         | Fecha a sessão de uma tarefa de agente interno          |
| `handoff`            | Registra a transferência para um humano ou outro agente |
| `heartbeat`          | Contadores do SDK, que medem a cobertura de cada fonte  |

O evento bruto é gravado antes da resposta. Você raramente monta o lote na mão: os SDKs guardam os eventos numa fila local e enviam em lotes, sem bloquear o agente.

<CodeGroup>
  ```python Python theme={null}
  from niadra import Niadra, phone

  niadra = Niadra(channel="whatsapp")

  niadra.track({
      "conversation_id": "wa-8812",
      "idempotency_key": message["id"],  # o id da mensagem no provedor
      "handles": [phone("+14155550123")],
      "speaker": {"role": "customer"},
      "content": {"text": "The technician never showed up. I am calling you."},
  })
  ```

  ```typescript TypeScript theme={null}
  import { Niadra, handles } from "@niadra/sdk";

  const niadra = new Niadra();

  niadra.track({
    channel: "whatsapp",
    conversation_id: "wa-8812",
    idempotency_key: message.id, // o id da mensagem no provedor
    handles: [handles.phone("+14155550123")],
    speaker: "customer",
    text: "The technician never showed up. I am calling you.",
  });
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/batch" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "items": [
      {
        "type": "event",
        "kind": "message",
        "idempotency_key": "wamid.HBgLMTQxNTU1NTAxMjMVAgASGBQzQUQ",
        "channel": "whatsapp",
        "conversation_id": "wa-8812",
        "handles": [{"type": "phone_e164", "value": "+14155550123"}],
        "speaker": {"role": "customer"},
        "content": {"type": "text", "text": "The technician never showed up. I am calling you."},
        "occurred_at": "2026-09-22T17:02:11Z"
      }
    ]
  }'
  ```
</CodeGroup>

## Idempotência

Todo item carrega um `idempotency_key`. Use o id da mensagem no provedor sempre que houver um: provedores de canal reenviam webhooks por dias, e o mesmo id nunca vira dois eventos. Quando não há id, o SDK gera um UUIDv7, que também ordena por tempo.

A deduplicação é pelo id, nunca por semelhança de conteúdo. Duas mensagens curtas iguais ("ok", "sim") são duas mensagens. Um item repetido volta contado em `duplicates`, sem erro.

## Um item ruim não derruba o lote

O lote responde **200** quando todos os itens entraram e **207** quando algum foi recusado, sempre com o resultado por item. O que passou conta em `accepted`, o que já existia conta em `duplicates`, e cada item recusado aparece em `errors` com a posição e o código:

```json theme={null}
{
  "accepted": 498,
  "duplicates": 1,
  "errors": [
    { "index": 17, "code": "verification_not_allowed", "detail": "V4 is above the ceiling of this source" }
  ]
}
```

A validação acontece em dois tempos: a borda confere só a forma do envelope, e cada item é conferido depois. As regras de forma, que os SDKs também aplicam antes de enviar:

* mensagem precisa de texto, transcrição ou referência de mídia;
* evento de sistema precisa de `canonical_type`, como `invoice.credited`;
* ação precisa do bloco `action`, e o bloco só vale quando `kind` é `action`;
* todo evento precisa de ao menos um handle, sujeito ou objeto.

Um item acima de 1 MB volta com o código `too_large`. Um corpo acima de 2,5 MB, ou um lote com mais de 500 itens, é recusado inteiro com 422 `invalid_input`.

O SDK tenta de novo com recuo em falha de rede, 429 e 503, e nunca repete um 4xx de validação.

## Conversas, tarefas e sessões

O `conversation_id` é seu: pode ser o thread do WhatsApp, que dura meses, ou o id da chamada. A Niadra divide cada conversa em **sessões**, janelas de atividade que fecham por inatividade do canal (cerca de 20 minutos no WhatsApp, 30 no app) ou por `conversation.ended`. Em voz, envie `conversation.ended` ao desligar. Uma chamada costuma ter dois ids, o da plataforma e o do tronco: mande o segundo em `conversation_aliases`.

Agente interno usa `task_id` no lugar da conversa. A tarefa fecha com `task.ended` ou depois de 10 minutos sem atividade. Os SDKs cuidam disso com `conversation()` e `task()`, que emitem o fim quando o bloco termina.

Mensagem nova fica legível na camada quente em menos de 1 segundo, já no `live` do próximo contexto de outro canal. Ação e evento de sistema chegam ao contexto recompilado em menos de 10 segundos.

## Mídia e dado tardio

Mídia nunca viaja dentro do evento. Reserve um upload em [`POST /v1/media/uploads`](/api/media-uploads), com `content_type`, `size_bytes` (até 500 MB), o `sha256` dos bytes e, sempre que souber, o `subject` dono do arquivo: assim o arquivo fica guardado sob aquela pessoa, e apagá-la apaga o arquivo mesmo que nenhum evento aponte para ele. A resposta traz `upload_url`, `upload_headers` e `expires_at`. Envie os bytes com `PUT` para `upload_url` com **exatamente** os cabeçalhos de `upload_headers`: o armazenamento recusa qualquer outro byte. Depois mande o evento com `content.media_ref` e `content.media_sha256`. Os SDKs fazem os três passos numa chamada.

<CodeGroup>
  ```python Python theme={null}
  from niadra import Niadra, phone

  niadra = Niadra(channel="whatsapp")
  marina = phone("+14155550123")

  # Reserves the upload, PUTs the bytes with the exact headers, returns the reference
  media = niadra.upload_media(voice_note_bytes, "audio/ogg", subject=marina)

  if media:
      niadra.track({
          "conversation_id": "wa-8812",
          "handles": [marina],
          "speaker": {"role": "customer"},
          "content": {
              "type": "audio",
              "media_ref": media.media_ref,
              "media_sha256": media.media_sha256,
              "transcript": "The technician never showed up. I am calling you.",
          },
      })
  ```

  ```typescript TypeScript theme={null}
  import { Niadra, handles } from "@niadra/sdk";

  const niadra = new Niadra();
  const marina = handles.phone("+14155550123");

  // Reserves the upload, PUTs the bytes with the exact headers, returns the reference
  const { data: media } = await niadra.uploadMedia({ data: voiceNote, content_type: "audio/ogg", subject: marina });

  if (media) {
    niadra.track({
      channel: "whatsapp",
      conversation_id: "wa-8812",
      handles: [marina],
      speaker: "customer",
      content: {
        type: "audio",
        media_ref: media.media_ref,
        media_sha256: media.media_sha256,
        transcript: "The technician never showed up. I am calling you.",
      },
    });
  }
  ```

  ```bash cURL theme={null}
  BASE=https://acme-prod.us-east-1.api.niadra.com

  # 1. Reserve the upload
  curl -X POST "$BASE/v1/media/uploads" \
    -H "Authorization: Bearer $NIADRA_API_KEY" -H "Content-Type: application/json" \
    -d '{"content_type": "audio/ogg", "size_bytes": 48213, "sha256": "'"$(shasum -a 256 note.ogg | cut -d" " -f1)"'",
         "subject": {"type": "phone_e164", "value": "+14155550123"}}'
  # => {"media_ref": "...", "upload_url": "https://...", "upload_headers": {"Content-Type": "audio/ogg", ...}, "expires_at": "..."}

  # 2. PUT the bytes with every header of upload_headers, exactly as returned
  curl -X PUT "$UPLOAD_URL" -H "Content-Type: audio/ogg" -H "x-amz-checksum-sha256: <value from upload_headers>" --data-binary @note.ogg
  ```
</CodeGroup>

Dado que chega depois, como a transcrição completa de uma chamada, é evento novo, não atualização. Ele marca a conversa e a memória derivada é refeita com versão nova.

## Cobertura das fontes

O SDK envia um `heartbeat` periódico com quantos eventos mandou. A Niadra compara com o que recebeu e marca cada fonte como `ok` ou `silent`. O contexto informa essa cobertura em `coverage`, e uma fonte muda pode disparar um [gatilho](/concepts/triggers-and-webhooks). [`GET /v1/sources/coverage`](/api/sources-coverage) mostra cada fonte dia a dia, por até 90 dias: o que o SDK diz que mandou (`sent`), o que chegou (`received`), o que foi recusado (`rejected`) e o que chegou com um tipo que o seu mapeamento não cobre (`unmapped`).

## Qual contexto o agente usou

Quando um agente responde, o SDK carimba o turno dele com `context_stamp`: o `etag` do contexto que entrou no prompt e `injected_at`, o momento em que entrou. É assim que o [aproveitamento do contexto](/concepts/context-use) separa o contexto que chegou tarde do que chegou e não foi usado. Dentro de um `conversation()`, os SDKs carimbam sozinhos quando você chama `mark_injected()` em Python ou `markInjected()` em TypeScript; só mande o carimbo à mão quando montar os eventos você mesmo.

```json theme={null}
{
  "type": "event",
  "idempotency_key": "call-4471-t2",
  "channel": "voice",
  "conversation_id": "call-4471",
  "handles": [{"type": "phone_e164", "value": "+14155550123"}],
  "speaker": {"role": "ai_agent"},
  "content": {"text": "Hi Marina, I can see the $40 credit on your August bill."},
  "context_stamp": {"etag": "\"cp1-9f2c4e\"", "injected_at": "2026-09-22T17:07:04Z"},
  "occurred_at": "2026-09-22T17:07:06Z"
}
```

## Arquivos: semear identidade ou trazer histórico

Para volume que não cabe no caminho da conversa, [`POST /v1/ingest/files`](/api/ingest-files) recebe um arquivo inteiro de até 512 MB, com o escopo `track`, e responde **202** com um `import_id`:

* `text/csv` semeia identidade. O cabeçalho nomeia tipos de handle (`phone_e164`, `email`, `system_id`...), com `<type>_scope` para o namespace, `<type>_2` a `<type>_9` para mais valores do mesmo tipo e os opcionais `idempotency_key`, `occurred_at` e `subject_kind`. Cada linha vira um `identify` com o método `system_import`, sob as mesmas proteções de qualquer outro.
* `application/x-ndjson` traz histórico: cada linha é um item de `POST /v1/batch`, validado do mesmo jeito.

```bash theme={null}
curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/ingest/files" \
  -H "Authorization: Bearer $NIADRA_API_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @crm-customers.csv
```

Acompanhe a importação por [`GET /v1/ingest/files/{import_id}`](/api/ingest-file): `status` (`queued`, `running`, `completed`, `failed`), `records`, `accepted`, `duplicates`, `rejected` e os 100 primeiros erros de linha, que trazem o número da linha e nunca o valor.

## Correções

Uma correção também é evento. [`POST /v1/feedback`](/api/feedback) recebe uma `action`, e cada ação pede os próprios campos: `retract_fact` pede `fact_id`; `correct_fact` pede `fact_id` e `value`; `resolve_open_item` pede `open_item_id`; `conversation_outcome` pede `conversation_id` e `value`. Um pedido sem eles é recusado com 422, em vez de ser gravado e ignorado. A resposta tem o formato da resposta do lote. Os dois SDKs têm `feedback()`.

## Próximos passos

<CardGroup cols={2}>
  <Card title="Identidade e verificação" href="/concepts/identity">
    como os handles viram um cliente só.
  </Card>

  <Card title="Sistemas, objetos e ações" href="/concepts/systems">
    eventos de ERP e ações que fecham pendências.
  </Card>

  <Card title="Enviar um lote" href="/api/batch">
    a referência completa de `POST /v1/batch`.
  </Card>
</CardGroup>
