> ## 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.

# Voice agents

> Context before hello within 150 ms, network attestation, two-pass transcripts and handoff.

A voice agent has less time than any other agent. The caller hears silence while the prompt is built, so the context has to be ready before the first word, and every lookup during the call has to fit inside a spoken turn. This guide wires a voice agent to Niadra from the moment the phone rings to the moment the call ends: context during the ring, history lookups sized for speech, the call metadata that proves who is calling, the transcript in two passes and the handoff to a human.

We follow Marina Souza. At 2:02 pm she complained on WhatsApp that the technician never showed up. At 2:06 pm the billing agent posted a \$40 credit on invoice 0823. At 2:07 pm she calls. The voice agent answers already knowing both.

## What the voice agent receives

The `voice` view is the shortest channel view. It carries who the caller is, what is still open, what other agents just did and the history highlights that change the conversation, sized so the agent can read it before it speaks. Anything that happened on another channel after the pack was compiled arrives in `live`, and it goes at the end of the prompt.

| Budget                                | Value  | Where it is set                               |
| ------------------------------------- | ------ | --------------------------------------------- |
| `context()` time budget, voice        | 150 ms | SDK `Timeouts.context_voice` / `contextVoice` |
| History navigation time budget, voice | 300 ms | SDK `navigation_voice` / `navigationVoice`    |
| Tokens returned by a history search   | 300    | `max_tokens=300`                              |

The SDK keeps its own time budget, independent of the voice platform. If the budget runs out, the call carries on and the agent speaks with what it has.

## Steps

### 1. Read the context while the phone rings

Start the conversation as soon as your voice platform reports the inbound call, and ask for the context in the same handler. By the time the call is answered, the pack is in the prompt. The calling number is the subject; the platform call id is your `conversation_id`.

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

  niadra = Niadra(channel="voice")

  def on_incoming_call(call):
      conversation = niadra.conversation(
          "call-4471",
          subject=phone(call.from_number),  # +14155550123
          view="voice",
          verification="V1",
      )
      ctx = conversation.context()
      call.agent.set_instructions(f"{AGENT_INSTRUCTIONS}\n\n{ctx.system_block}")
      return conversation
  ```

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

  const niadra = new Niadra();

  async function onIncomingCall(call: IncomingCall) {
    const conversation = niadra.conversation({
      subject: handles.phone(call.fromNumber), // +14155550123
      channel: "voice",
      conversation_id: "call-4471",
      verification: "V1",
    });
    const ctx = await conversation.context();
    await call.agent.setInstructions(`${AGENT_INSTRUCTIONS}\n\n${ctx.text}`);
    return conversation;
  }
  ```

  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/context" \
    -H "Authorization: Bearer $NIADRA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "subject": { "type": "phone_e164", "value": "+14155550123" },
      "view": "voice",
      "verification": "V1",
      "conversation_id": "call-4471"
    }'
  ```
</CodeGroup>

With `conversation_id`, the server pins the pack: every turn of this call gets the same bytes, so the model provider keeps the prompt prefix cached. What changes during the call (a new action by another agent, a message on WhatsApp) comes as a delta and as live turns. In Python, append `ctx.turn_block` at the end of each turn; in TypeScript, append `ctx.suffix`.

<Tip>
  In TypeScript, a conversation on channel `voice` uses the `voice` view by default. Pass `target` with the provider and model of your speech agent when you know it, so the pack is sized for that model's prompt cache.
</Tip>

### 2. Send the call metadata

The verification level is what the call proved, never a guess. Network attestation from the carrier (STIR/SHAKEN and its equivalents) is the strongest signal a call carries before any question is asked. Level A maps to V2; levels B and C map to V1. A hidden caller id or a PBX number is V0 or V1, and the agent identifies the person by conversation.

Send the call details in the `voice` block of the first event. A call often has two ids, the platform id and the trunk id: put the second one in `conversation_aliases` so both resolve to the same conversation.

<CodeGroup>
  ```python Python theme={null}
  conversation.customer(
      "Hi, it's Marina. The technician never showed up this morning.",
      conversation_aliases=["trunk-7f2a9"],
      voice={
          "ani": "+14155550123",
          "dnis": "+18005550100",
          "trunk": "sip-east-2",
          "network_attestation": "A",
          "answered_at": "2026-09-22T17:07:03Z",
          "turn_offset_ms": 2400,
      },
  )
  ```

  ```typescript TypeScript theme={null}
  conversation.track({
    speaker: "customer",
    text: "Hi, it's Marina. The technician never showed up this morning.",
    conversation_aliases: ["trunk-7f2a9"],
    voice: {
      ani: "+14155550123",
      dnis: "+18005550100",
      trunk: "sip-east-2",
      network_attestation: "A",
      answered_at: "2026-09-22T17:07:03Z",
      turn_offset_ms: 2400,
    },
  });
  ```
</CodeGroup>

The effective level is always the lowest of three values: what you requested, the ceiling of your source and what the conversation proved. An automated agent source has a ceiling of V2. The response tells you what happened in `verification.requested`, `verification.effective` and `verification.reason`.

### 3. Let the agent look up the history, sized for speech

The pack answers the most common question on its own: its "From the history" section says whether this happened before and how it was settled. When the caller brings up something older, the agent searches. Bind the tools to the caller in your code, so the model chooses the query and never the customer, and use the voice budget.

<CodeGroup>
  ```python Python theme={null}
  kit = niadra.tools(phone("+14155550123"), conversation_id="call-4471", voice=True)

  # Hand kit.definitions to your speech model; route its tool calls here:
  output = kit.call("search_customer_history", {"query": "credit for missed technician visit"})
  ```

  ```typescript TypeScript theme={null}
  const kit = conversation.tools(); // bound to the caller, voice budget

  // Hand kit.definitions to your speech model; route its tool calls here:
  const output = await kit.call("search_customer_history", { query: "credit for missed technician visit" });
  ```
</CodeGroup>

A search called from a voice conversation returns up to 300 tokens, cut by value, with a `recurrence` block when the query matches a category: "second missed visit in 12 months; last time, a \$40 credit". Literal transcript excerpts are never returned to a voice audience.

### 4. Record what the agent says

Capture the agent turns as they happen. Call `mark_injected()` (Python) or `markInjected()` (TypeScript) each time the pack goes into the prompt: the agent's next turns and actions then carry a `context_stamp` with the ETag of that pack and the moment it went in, which is how the [context use](/en/concepts/context-use) measurement tells a late context from an unused one. If your agent is built on the OpenAI client, `wrap()` in either SDK injects the pack, stamps it and records the answers for you.

<CodeGroup>
  ```python Python theme={null}
  ctx = conversation.context()
  conversation.mark_injected(ctx)  # the pack went into this prompt
  conversation.agent("Marina, I can see the $40 credit on your August bill was already applied at 2:06 pm.")
  ```

  ```typescript TypeScript theme={null}
  const ctx = await conversation.context();
  conversation.markInjected(ctx); // the pack went into this prompt
  conversation.agent("Marina, I can see the $40 credit on your August bill was already applied at 2:06 pm.");
  ```
</CodeGroup>

### 5. Hand off to a human, warm

When the caller asks for a person, record the handoff before the transfer. With `mode="warm"`, the receiving desk reads the `brief` view: a short, spoken-length briefing your platform can whisper to the attendant. The attendant keeps working in the tool your company already uses; the context reaches it by API, webhook or MCP.

<CodeGroup>
  ```python Python theme={null}
  conversation.handoff("human", target_source="src_service_desk", reason="asked for a person", mode="warm")

  brief = niadra.context(subject=phone("+14155550123"), view="brief", conversation_id="call-4471")
  ```

  ```typescript TypeScript theme={null}
  await conversation.handoff({ target: "human", target_source: "src_service_desk", reason: "asked for a person", mode: "warm" });

  const brief = await niadra.context({ subject: handles.phone("+14155550123"), view: "brief", conversation_id: "call-4471" });
  ```
</CodeGroup>

A transfer whose destination never reads the context within 10 minutes shows up as a handoff without reading in the measurement.

### 6. End the call, then send the final transcript

End the conversation the moment the call hangs up. In Python, leaving the `with` block does it; you can also call `end()`. That closes the session at once instead of waiting for inactivity, so the derived memory is ready in under a minute.

<CodeGroup>
  ```python Python theme={null}
  niadra.track({
      "channel": "voice",
      "conversation_id": "call-4471",
      "handles": [phone("+14155550123")],
      "speaker": {"role": "system"},
      "kind": "system_event",
      "canonical_type": "call.ended",
      "voice": {"ended_at": "2026-09-22T17:11:40Z", "end_reason": "caller_hangup", "recording_ref": "rec_88213"},
  })
  conversation.end()
  ```

  ```typescript TypeScript theme={null}
  conversation.track({
    speaker: "system",
    kind: "system_event",
    canonical_type: "call.ended",
    voice: { ended_at: "2026-09-22T17:11:40Z", end_reason: "caller_hangup", recording_ref: "rec_88213" },
  });
  await conversation.end();
  ```
</CodeGroup>

Voice platforms deliver in two passes: the real-time transcript during the call and a better one after it. Send the post-call transcript as new events on the same `conversation_id`, with their own idempotency keys. Late data is never an update: it marks the conversation for a new extraction, and the episode gets a new version. Turns with speech-to-text confidence below the threshold do not count as questions or claims in the measurement.

<Note>
  The audio itself never travels inside an event. Upload the recording with `upload_media()` in Python or `uploadMedia()` in TypeScript, passing the caller as `subject` so erasing the customer also erases the recording, and put the returned `media_ref` in `voice.recording_ref` or `content.media_ref`, with its SHA-256. Over HTTP, [`POST /v1/media/uploads`](/en/api/media-uploads) returns the URL and the exact `upload_headers` to send with the `PUT`.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Context and views" href="/en/concepts/context">
    layers, pinning, live turns and delta.
  </Card>

  <Card title="Identity and verification" href="/en/concepts/identity">
    how V0 to V4 are proven and capped.
  </Card>

  <Card title="History navigation" href="/en/concepts/history">
    search, timeline and open, with budgets.
  </Card>

  <Card title="WhatsApp agents" href="/en/guides/whatsapp-agents">
    the other side of the 2:02 pm message.
  </Card>
</CardGroup>
