# Vapi

> Move a Vapi assistant onto FirstByte. The assistant becomes a console voice agent. Server-URL webhooks become signed platform webhooks. Custom functions become HTTP agent tools.

You have a Vapi assistant. It is a hosted voice pipeline with a system prompt
and model choices. It has a **server URL** that receives call events and
function calls, and a phone-call API. Every piece has a direct counterpart on
FirstByte. Unlike the code-first integrations (LiveKit, Pipecat), most of
this port is **configuration**, not code. The only component that you continue
to run is the server that backs your webhooks and tools.

Your Vapi setup can instead pipe raw audio to your own model (a custom
transcriber/voice over WebSocket). In that case, use the **external agent**
seam. Your process holds one outbound QUIC/WebTransport session. It
authenticates with a media key/secret from the console's External-agent create
flow. It receives each call's 8 kHz `pcm16` audio directly. The
[external-agent recipe](/modalities/voice/recipes/livekit-transport) covers
that path end to end.

## What maps to what

| Vapi | FirstByte |
| --- | --- |
| Assistant (prompt, STT/LLM/TTS choices) | A voice agent in the console. The managed runtime runs the cascade |
| Server URL receiving call events | `webhooks.create {vertical:'voice', url, eventTypes}`, with signed deliveries |
| `status-update` / `end-of-call-report` messages | `voice.call.*` events and `voice.cdr.created` |
| Call recording / transcript in the report | `voice.recording.ready` / `voice.transcript.ready` events |
| Custom functions on the server URL | HTTP **agent tools** (`admin.upsertAgentTool`). The runtime POSTs arguments to your route mid-call |
| `POST /call` (phone-call API) | `voice.calls.originate {to, trunkId, agent?}` |
| Vapi phone numbers | Your DIDs on a [SIP trunk](/modalities/voice/transport-telephony/sip-trunking) |
| Custom transcriber / custom voice over WS | External agent. Your worker owns the audio in-process |

## Port the assistant

The runnable starter is a port of Vapi's server-example (Flask). It contains
the webhook route, the two custom functions, and a provisioning script. It
ships in the provider starters repo next to the `clutchcall-agents` SDK.

  1. **Recreate the assistant as a voice agent**
In the console (`agent.firstbyte.fast`), create a voice agent. Copy over
    the assistant's system prompt and STT/LLM/TTS choices. Provider keys are
    per-tenant. The control plane seals them at rest and resolves them at call
    setup. See
    [BYO ASR / LLM / TTS](/modalities/voice/runtime/byo-asr-llm-tts) for the
    provider matrix.

  2. **Register your server URL as a webhook**
Management operations use an `mpk_` API key (portal → Settings → API
    Keys) against `POST /mcp`. The SDK wraps the endpoint:

```python provision.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")

api.call("webhooks.create", {
    "vertical": "voice",
    "url": "https://your-app.example/webhook",
    "eventTypes": ["voice.call.started", "voice.call.answered",
                   "voice.call.ended", "voice.call.failed",
                   "voice.call.transferred", "voice.cdr.created",
                   "voice.recording.ready", "voice.transcript.ready"],
})
```

    The API returns the signing secret (`whsec_…`) only **one time**, on
    creation.

  3. **Verify deliveries where Vapi sent messages**
Deliveries are signed Stripe-style. `X-Clutchcall-Signature` carries
    `t=<epoch>,v1=<hmac_sha256(secret, "t.body")>`. Your route changes from
    "switch on `message.type`" to "verify, then switch on the event header":

```python webhook.py
from clutchcall_agents.webhooks import (
    SIGNATURE_HEADER, EVENT_HEADER, WebhookVerificationError, verify_signature,
)

@app.route("/webhook", methods=["POST"])
def webhook_route():
    try:
        verify_signature(os.environ["CLUTCHCALL_WEBHOOK_SECRET"],
                         request.headers.get(SIGNATURE_HEADER, ""),
                         request.get_data())
    except WebhookVerificationError:
        return "", 401

    event = request.headers.get(EVENT_HEADER, "")   # e.g. voice.call.answered
    payload = request.get_json()
    ...
```

  4. **Turn custom functions into agent tools**
Vapi POSTs `function-call` messages to your server URL. Here, each
    function is an **HTTP agent tool** attached to the agent. The runtime's
    LLM calls the tool mid-call. The platform POSTs the arguments to your
    route. The agent speaks the returned result:

```python
api.call("admin.upsertAgentTool", {
    "agentId": agent_id,
    "tool": {
        "kind": "http",
        "name": "getRandomName",
        "description": "Fetches a random person's name.",
        "spec": {
            "method": "POST",
            "path": "/functions/get_random_name",
            "parameters": {"type": "object", "properties": {
                "gender": {"type": "string", "enum": ["male", "female"]},
            }},
        },
    },
})
```

    Your Flask/Express handlers for the functions do not change. Only the
    registration moves. See
    [Tool Calling](/modalities/voice/runtime/tool-calling) for the full spec.

  5. **Replace the phone-call API**
Vapi's `POST /call` with an `assistantId` becomes one operation with an
    agent name. On answer, the engine connects the callee to that agent:

```python
api.call("voice.calls.originate",
         {"to": "+15552223333", "trunkId": "trunk_main", "agent": "vapi-port"})
```

## Troubleshooting

  **Webhook deliveries are rejected with 401**

Check the signature against the **exact raw request body**. A re-parsed
    and re-serialized JSON body computes a different HMAC. Also check clock
    skew: `verify_signature` enforces a freshness window on the `t=`
    timestamp.

  **The agent never calls a tool**

The tool's `description` and parameter schema drive tool selection,
    exactly as with Vapi's function definitions. Check that the tool upserted
    onto the correct `agentId`. Check that your endpoint returns quickly. A
    slow tool stalls the turn.

  **You need the raw audio, not the managed pipeline**

That is the external-agent seam, not a console agent. Your worker
    registers under an agent handle. The engine routes calls through
    `VENDOR_BRIDGE`. You read 20 ms `pcm16` frames in-process. Start from the
    [external-agent recipe](/modalities/voice/recipes/livekit-transport).

## Related

  - **[LiveKit integration](/modalities/voice/integrations/livekit)** — The external-agent seam in depth, for code-first runtimes.
  - **[Tool Calling](/modalities/voice/runtime/tool-calling)** — HTTP agent tools: schemas, silent tools, and mid-call behavior.
  - **[SIP Trunking](/modalities/voice/transport-telephony/sip-trunking)** — Bring the numbers that your Vapi assistant answers today.
  - **[Run a LiveKit Agent on the Transport](/modalities/voice/recipes/livekit-transport)** — The end-to-end external-agent recipe.
