# Plivo

> Move a Plivo Voice app onto FirstByte. The answer-URL/XML model becomes in-process call handling in an external agent. The REST surface maps to management-API operations.

You have a Plivo Voice app. Plivo's model is an **answer URL**. Their cloud
calls your web server. Your server returns XML verbs: `<Speak>`, `<Play>`,
`<GetDigits>`, `<Dial>`. Plivo executes those verbs against the call.
FirstByte inverts that model. Your code runs as an **external agent**. The
external agent is a worker. The worker dials **one outbound QUIC/WebTransport
session** to the engine. It authenticates with a media app key/secret. It
registers under an agent handle. The engine routes calls to the worker through
a `VENDOR_BRIDGE` agent config. It hands each live call to your function.
Audio flows in both directions as 20 ms `pcm16` frames at 8 kHz.

You do not need Flask, a public URL, or ngrok. You do not return XML for a
remote executor to interpret. Your process **is** the executor.

## What maps to what

| Plivo | FirstByte |
| --- | --- |
| answer URL returning XML | none. Route the number to your external agent in the console |
| `<Speak>Hello…</Speak>` | `await call.send_audio(pcm)`. Synthesize with any TTS that you like |
| `<Play>file.mp3</Play>` | `call.send_audio()` of a decoded 8 kHz mono `pcm16` WAV |
| `<GetDigits>` / `<GetInput>` | read `call.audio()`. DTMF and speech both arrive as audio frames |
| `<Dial><Number>…</Number></Dial>` | `voice.calls.transfer` on the management API |
| end of XML document → hangup | return from your handler → the call ends |
| `RestClient().calls.create(from_, to_, answer_url)` | `voice.calls.originate {to, trunkId, agent?}` |
| hangup / answer callbacks | `voice.call.*` webhooks, signed and verified |
| `request.values["From"]` | `call.caller_number` (and `call.called_number` for the DNIS) |

## Port the voice quickstarts

The runnable starter is a port of Plivo's Python voice quickstarts
(`greet_caller.py`, `receive_calls.py`, `forward_calls.py`,
`text_to_speech.py`). It ships in the provider starters repo next to the
`clutchcall-agents` SDK.

  1. **Install the SDK**
```bash
pip install "clutchcall-agents[quic]" \
  --extra-index-url https://artifacts.clutchcall.dev/pip/simple/
```

  2. **Replace the answer URL with a handler**
Plivo's greet-the-caller flow (answer, `<Speak>` a greeting, hang up)
    becomes one coroutine:

```python greet_caller.py
from clutchcall_agents import AgentConfig, Call, run

GREETING = load_wav("greeting.wav")   # 8 kHz mono pcm16

async def greet_caller(call: Call):
    print(f"Incoming call from {call.caller_number} to {call.called_number}")
    await call.send_audio(GREETING)   # one shot; the SDK paces 20 ms frames
    await call.flush()                # mark the segment for playout reporting
    call.send_transcript("assistant", "Hello, welcome!")
    await wait_for_playout(call)      # let it play, then return = hangup

run(greet_caller, AgentConfig.from_env())
```

    `send_transcript` lands the turn in the platform transcript store. It
    fires the `voice.transcript.ready` webhook. Plivo's XML model never had
    this piece.

  3. **Create the external agent and route a DID**
The console's **External agents** create flow (`agent.firstbyte.fast`)
    does three tasks:

    - It reserves the agent handle.
    - It mints the media key/secret. `AgentConfig.from_env()` reads them from
      `CLUTCHCALL_MEDIA_KEY` / `CLUTCHCALL_MEDIA_SECRET`.
    - It writes the `VENDOR_BRIDGE` config.

    Route your number to the agent (see
    [SIP Trunking](/modalities/voice/transport-telephony/sip-trunking)). Then
    dial the number.

  4. **Port the REST calls**
Management operations use an `mpk_` API key (portal → Settings → API
    Keys) against `POST /mcp`. The SDK wraps it. Plivo's
    `calls.create(..., answer_url=...)` becomes `originate` with an `agent`.
    On answer, the engine connects the callee to your worker:

```python originate_call.py
from clutchcall_agents.api import ClutchCallAPI

api = ClutchCallAPI(base_url="https://app.firstbyte.fast",
                   api_key=os.environ["CLUTCHCALL_API_KEY"],  # mpk_...
                   org_id="org_abc")

call = api.call("voice.calls.originate",
                {"to": "+15552223333", "trunkId": "trunk_main",
                 "agent": "plivo-quickstart"})
print(call["sid"])
```

    The `forward_calls.py` flow (`<Dial>`) is `voice.calls.transfer` with the
    call's sid. `voice.calls.hangup` ends a leg. For lifecycle events
    (answer/hangup callbacks), register a signed webhook with
    `webhooks.create {vertical:'voice', url, eventTypes}`. Check deliveries
    with `clutchcall_agents.webhooks.verify_signature`.

## Troubleshooting

  **The greeting plays at the wrong speed**

Your WAV is not 8 kHz mono 16-bit PCM. `send_audio` is a raw passthrough
    at the call's sample rate. Resample the file offline. Do not send
    44.1 kHz samples onto an 8 kHz leg.

  **The call hangs up before the greeting finishes**

A return from the handler ends the call (Plivo's implicit end-of-XML).
    `send_audio` queues audio. Wait for playout, or for the caller, before
    you return.

  **The worker registers, but inbound calls never arrive**

`vendor_room` in the agent config must exactly match the handle that the
    worker registered (`CLUTCHCALL_AGENT`). The DID must route to that agent.
    Presence is tied to the live session. Check for a ready line after the
    most recent reconnect.

## Related

  - **[LiveKit integration](/modalities/voice/integrations/livekit)** — The external-agent seam in depth: the same worker pattern with a full
    agent framework on top.
  - **[Run a LiveKit Agent on the Transport](/modalities/voice/recipes/livekit-transport)** — The end-to-end external-agent recipe: register a worker, route a number,
    take a call.
  - **[SIP Trunking](/modalities/voice/transport-telephony/sip-trunking)** — Bring your numbers over, or point a Plivo SIP trunk at the platform.
  - **[Telnyx integration](/modalities/voice/integrations/telnyx)** — The same port for Call Control's command/webhook model.
