|
| 1 | +--- |
| 2 | +title: Assistant Request |
| 3 | +subtitle: Learn how inbound resources fetch assistants dynamically from your server. |
| 4 | +slug: webhooks/assistant-request |
| 5 | +--- |
| 6 | + |
| 7 | +The Assistant Request webhook lets you decide which assistant (or transfer destination) should handle an inbound phone call at runtime. When a phone number or org has a **Server URL** configured, Vapi pauses the call setup, POSTs an `assistant-request` message to your server, and expects you to reply with the assistant configuration to use. |
| 8 | + |
| 9 | +## Prerequisites |
| 10 | + |
| 11 | +1. A phone number imported into your [Vapi Dashboard](https://dashboard.vapi.ai). |
| 12 | +2. A publicly reachable HTTPS endpoint (ngrok, Cloudflare tunnel, etc.). |
| 13 | +3. Optional: a shared secret or webhook credential to authenticate requests. |
| 14 | + |
| 15 | +## Step 1: Configure the Number |
| 16 | + |
| 17 | +1. Open **Phone Numbers** → select your number. |
| 18 | +2. Clear the **Assistant** field (so Vapi relies on the webhook). |
| 19 | +3. Under **Server URL**, enter your tunnel URL (e.g., `https://example.ngrok-free.app/webhook/vapi`). |
| 20 | +4. Set a **Server Secret** or attach a **Custom Credential** for request verification. |
| 21 | +5. Save. |
| 22 | + |
| 23 | +> **Note:** If you leave the phone number’s Server URL blank, Vapi automatically falls back to the org-level server settings (`org.server.url`, secret, headers, etc.). Assistant requests still fire as long as either the number or the org has a Server URL configured. |
| 24 | +
|
| 25 | +## Step 2: Build a Webhook Endpoint |
| 26 | + |
| 27 | +Any framework works; here’s a minimal Express server that logs the request and always replies with an existing assistant ID. |
| 28 | + |
| 29 | +```javascript |
| 30 | +import express from 'express'; |
| 31 | + |
| 32 | +const ASSISTANT_ID = 'your-assistant-id'; |
| 33 | +const SECRET = process.env.VAPI_SERVER_SECRET ?? 'my-secret'; |
| 34 | + |
| 35 | +const app = express(); |
| 36 | +app.use(express.json({ limit: '1mb' })); |
| 37 | + |
| 38 | +app.post('/webhook/vapi', (req, res) => { |
| 39 | + if (SECRET && req.headers['vapi-secret'] !== SECRET) { |
| 40 | + return res.status(401).json({ error: 'Unauthorized' }); |
| 41 | + } |
| 42 | + |
| 43 | + console.log('Assistant request:', JSON.stringify(req.body, null, 2)); |
| 44 | + res.json({ assistantId: ASSISTANT_ID }); |
| 45 | +}); |
| 46 | + |
| 47 | +app.listen(8000, () => |
| 48 | + console.log('Listening on http://localhost:8000/webhook/vapi'), |
| 49 | +); |
| 50 | +``` |
| 51 | +
|
| 52 | +## Step 3: Understand the Request Payload |
| 53 | +
|
| 54 | +The POST body always contains a `message` object whose `type` is `assistant-request`. Vapi includes call, phone number, and customer context so you can make routing decisions. |
| 55 | +
|
| 56 | +```524:533:libs/core/src/types/message.types.ts |
| 57 | +export class AssistantRequestMessage extends OutboundMessageBase { |
| 58 | + @IsIn(['assistant-request']) |
| 59 | + type: 'assistant-request'; |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +Sample payload (truncated for brevity): |
| 64 | + |
| 65 | +```json |
| 66 | +{ |
| 67 | + "message": { |
| 68 | + "timestamp": 1723595100000, |
| 69 | + "type": "assistant-request", |
| 70 | + "call": { "id": "call_uuid", "orgId": "org_uuid", "transport": {...} }, |
| 71 | + "phoneNumber": { "id": "pn_uuid", "number": "+15551234567" }, |
| 72 | + "customer": { "number": "+15559871234" } |
| 73 | + } |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +## Step 4: Respond With an Assistant |
| 78 | + |
| 79 | +Your JSON response must follow `AssistantRequestMessageResponse`, which extends the `CallAssistant` schema. That means you can return: |
| 80 | + |
| 81 | +- `assistantId`: use a saved assistant. |
| 82 | +- `assistant`: inline assistant definition (same structure as POST `/assistant`). |
| 83 | +- `squadId` / `squad` or `workflowId` / `workflow`. |
| 84 | +- `assistantOverrides`, `squadOverrides`, or `workflowOverrides`. |
| 85 | +- `destination`: transfer immediately to a number or SIP URI. |
| 86 | +- `error`: reject the call with a spoken message. |
| 87 | + |
| 88 | +```1679:1750:libs/core/src/types/message.types.ts |
| 89 | +export class AssistantRequestMessageResponse extends IntersectionType( |
| 90 | + OutboundMessageResponseBase, |
| 91 | + CallAssistant, |
| 92 | +) { |
| 93 | + destination?: TransferDestinationSip | TransferDestinationNumber; |
| 94 | + error?: string; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +Example responses: |
| 99 | + |
| 100 | +```json |
| 101 | +{ "assistantId": "2ccabf54-ccd8-4dff-ae93-e830159c8004" } |
| 102 | +``` |
| 103 | + |
| 104 | +```json |
| 105 | +{ |
| 106 | + "assistant": { |
| 107 | + "name": "Dynamic Intake", |
| 108 | + "model": { "provider": "openai", "model": "gpt-4o-mini" }, |
| 109 | + "voice": { "provider": "11labs", "voiceId": "..." } |
| 110 | + }, |
| 111 | + "assistantOverrides": { |
| 112 | + "variables": { "accountId": "12345" } |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +```json |
| 118 | +{ "destination": { "type": "number", "number": "+15551230000" } } |
| 119 | +``` |
| 120 | + |
| 121 | +```json |
| 122 | +{ "error": "All agents are busy. Please try again later." } |
| 123 | +``` |
| 124 | + |
| 125 | +## Assistant Request Flow |
| 126 | + |
| 127 | +The diagram below shows the synchronous decision tree: Vapi pauses the call, invokes your Assistant Request Server (ARS), optionally enriches context via CRM, and either transfers the call or resumes it with the returned assistant. |
| 128 | + |
| 129 | +```mermaid |
| 130 | +sequenceDiagram |
| 131 | + participant Caller |
| 132 | + participant Vapi |
| 133 | + participant ARS as "Assistant Request Server" |
| 134 | + participant CRM as "CRM (optional)" |
| 135 | + participant Transfer as "Transfer Destination (optional)" |
| 136 | +
|
| 137 | + %% 1. Caller calls Vapi |
| 138 | + Caller->>Vapi: Inbound phone call ("Connect me to support") |
| 139 | +
|
| 140 | + %% 2. Vapi self-note |
| 141 | + Note right of Vapi: Phone number lacks fixed assistant → trigger assistant request |
| 142 | +
|
| 143 | + %% 3. Vapi requests assistant |
| 144 | + Vapi->>ARS: POST /webhook/vapi<br/>(assistant-request payload with call + customer context) |
| 145 | +
|
| 146 | + %% 4. Optional CRM enrichment |
| 147 | + opt CRM enrichment (optional) |
| 148 | + ARS->>CRM: Lookup caller / apply business rules |
| 149 | + CRM-->>ARS: Customer profile / routing decision |
| 150 | + end |
| 151 | +
|
| 152 | + %% Optional CRM note |
| 153 | + Note over CRM: CRM step runs only when extra context is needed. |
| 154 | +
|
| 155 | + %% 5. ARS responds |
| 156 | + ARS-->>Vapi: assistantId | inline assistant | destination | error |
| 157 | +
|
| 158 | + %% 6. Vapi splits: forward vs assistant |
| 159 | + alt Response includes destination |
| 160 | + Vapi->>Transfer: Forward call to number/SIP destination |
| 161 | + Transfer-->>Caller: Connected to destination |
| 162 | + else Assistant provided (default) |
| 163 | + Vapi->>Caller: Assistant joins call using provided config |
| 164 | + end |
| 165 | +``` |
| 166 | + |
| 167 | +## Testing the Flow |
| 168 | + |
| 169 | +1. Start your webhook server. |
| 170 | +2. Ensure its public URL is reachable by Vapi (through your tunnel, reverse proxy, or production host). |
| 171 | +3. Place a call to the configured Vapi number. |
| 172 | +4. Monitor your server logs to verify the assistant-request payload arrives. |
| 173 | +5. Confirm the call proceeds according to the assistant or destination you returned. |
| 174 | + |
| 175 | +## Troubleshooting & Tips |
| 176 | + |
| 177 | +- **Timeouts:** The assistant request times out after ~5 s (`DEFAULT_TIMEOUT_SECOND_ASSISTANT_REQUEST`). Keep logic fast or cache lookups. |
| 178 | +- **Missing server URL:** Calls end immediately with `call-start-error-neither-assistant-nor-server-set`. |
| 179 | +- **Invalid response:** Vapi records ended reasons such as `assistant-request-returned-error` or `assistant-request-returned-no-assistant`. Check the call log and your server logs. |
| 180 | +- **Authentication failures:** Ensure the secret/credential in the phone number matches what you validate in the webhook. |
| 181 | +- **Debugging:** Use the ngrok inspector (`http://127.0.0.1:4040`) to inspect payloads, or log the entire request as shown above. |
| 182 | + |
| 183 | +By handling the assistant request webhook, you can dynamically route every inbound interaction—pick the right assistant per customer, run A/B tests, or short-circuit calls to human teams when necessary. |
0 commit comments