| title | Local webhook testing |
|---|---|
| description | Forward webhooks to your local development server with vapi listen |
| slug | cli/webhook |
The vapi listen command provides a local webhook forwarding service that receives events and forwards them to your local development server. This helps you debug webhook integrations during development.
Important: vapi listen does NOT provide a public URL or tunnel. You'll need to use a separate tunneling solution like ngrok to expose your local server to the internet.
In this guide, you'll learn to:
- Set up local webhook forwarding with a tunneling service
- Debug webhook events in real-time
- Configure advanced forwarding options
- Handle different webhook types
Note the public URL provided by your tunneling service (e.g., `https://abc123.ngrok.io`)
This starts a local server on port 4242 that forwards to your application
- Use a tunneling service (ngrok, localtunnel, etc.) to expose port 4242 to the internet
- Configure your Vapi webhook URLs to point to the tunnel URL
- The flow is: Vapi → Your tunnel URL → vapi listen (port 4242) → Your local server
Forward to your local development server:
# Forward to localhost:3000/webhook
vapi listen --forward-to localhost:3000/webhook
# Short form
vapi listen -f localhost:3000/webhookUse a different port for the webhook listener:
# Listen on port 8080 instead of default 4242
vapi listen --forward-to localhost:3000/webhook --port 8080
# Remember to update your tunnel to use port 8080
ngrok http 8080For development with self-signed certificates:
vapi listen --forward-to https://localhost:3000/webhook --skip-verifyWhen you run vapi listen, you'll see:
$ vapi listen --forward-to localhost:3000/webhook
🎧 Vapi Webhook Listener
📡 Listening on: http://localhost:4242
📍 Forwarding to: http://localhost:3000/webhook
⚠️ To receive Vapi webhooks:
1. Use a tunneling service (e.g., ngrok http 4242)
2. Update your Vapi webhook URLs to the tunnel URL
Waiting for webhook events...
[2024-01-15 10:30:45] POST /
Event: call-started
Call ID: call_abc123def456
Status: 200 OK (45ms)
[2024-01-15 10:30:52] POST /
Event: speech-update
Transcript: "Hello, how can I help you?"
Status: 200 OK (12ms)The listener forwards all Vapi webhook events:
- `call-started` - Call initiated - `call-ended` - Call completed - `call-failed` - Call encountered an error - `speech-update` - Real-time transcription - `transcript` - Final transcription - `voice-input` - User speaking detected - `function-call` - Tool/function invoked - `assistant-message` - Assistant response - `conversation-update` - Conversation state change - `error` - Error occurred - `recording-ready` - Call recording available - `analysis-ready` - Call analysis completeThe listener adds helpful headers to forwarded requests:
X-Forwarded-For: vapi-webhook-listener
X-Original-Host: <your-tunnel-domain>
X-Webhook-Event: call-started
X-Webhook-Timestamp: 1705331445Your server receives the exact webhook payload from Vapi with these additional headers for debugging.
```bash # Terminal 1: Start ngrok tunnel ngrok http 4242# Terminal 2: Start vapi listener
vapi listen --forward-to localhost:3000/webhook
# Use the ngrok URL in Vapi Dashboard
```
# Terminal 2: Start vapi listener
vapi listen --forward-to localhost:3000/webhook
# Use the localtunnel URL in Vapi Dashboard
```
# Terminal 2: Start vapi listener
vapi listen --forward-to localhost:3000/webhook
# Use the cloudflare URL in Vapi Dashboard
```
Filter specific event types (coming soon):
# Only forward call events
vapi listen --forward-to localhost:3000 --filter "call-*"
# Multiple filters
vapi listen --forward-to localhost:3000 --filter "call-started,call-ended"The listener expects standard HTTP responses:
- 200-299: Success, event processed
- 400-499: Client error, event rejected
- 500-599: Server error, will retry
console.log(Webhook received: ${type} at ${timestamp});
switch (type) {
case 'call-started':
console.log(Call ${call.id} started with ${call.customer.number});
break;
case 'speech-update':
console.log(`User said: ${req.body.transcript}`);
break;
case 'function-call':
const { functionName, parameters } = req.body.functionCall;
console.log(`Function called: ${functionName}`, parameters);
// Return function result
const result = await processFunction(functionName, parameters);
return res.json({ result });
case 'call-ended':
console.log(`Call ended. Duration: ${call.duration}s`);
break;
}
res.status(200).send(); });
```python title="Python/FastAPI"
from fastapi import FastAPI, Request
from datetime import datetime
app = FastAPI()
@app.post("/api/vapi/webhook")
async def handle_webhook(request: Request):
data = await request.json()
event_type = data.get("type")
call = data.get("call", {})
timestamp = data.get("timestamp")
print(f"Webhook received: {event_type} at {timestamp}")
if event_type == "call-started":
print(f"Call {call.get('id')} started")
elif event_type == "speech-update":
print(f"User said: {data.get('transcript')}")
elif event_type == "function-call":
function_call = data.get("functionCall", {})
function_name = function_call.get("functionName")
parameters = function_call.get("parameters")
# Process function and return result
result = await process_function(function_name, parameters)
return {"result": result}
elif event_type == "call-ended":
print(f"Call ended. Duration: {call.get('duration')}s")
return {"status": "ok"}
func handleWebhook(c *gin.Context) {
var data map[string]interface{}
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
eventType := data["type"].(string)
fmt.Printf("Webhook received: %s\n", eventType)
switch eventType {
case "call-started":
call := data["call"].(map[string]interface{})
fmt.Printf("Call %s started\n", call["id"])
case "speech-update":
fmt.Printf("User said: %s\n", data["transcript"])
case "function-call":
functionCall := data["functionCall"].(map[string]interface{})
result := processFunction(
functionCall["functionName"].(string),
functionCall["parameters"],
)
c.JSON(200, gin.H{"result": result})
return
case "call-ended":
fmt.Println("Call ended")
}
c.JSON(200, gin.H{"status": "ok"})
}Test error handling in your webhook:
# Your handler returns 500
vapi listen --forward-to localhost:3000/webhook-error
# Output shows:
# Status: 500 Internal Server Error (23ms)
# Response: {"error": "Database connection failed"}Test with multiple concurrent calls:
# Terminal 1: Start listener
vapi listen --forward-to localhost:3000/webhook
# Terminal 2: Trigger multiple calls via API
for i in {1..10}; do
vapi call create --to "+1234567890" &
doneFilter logs by call ID:
# Coming soon
vapi listen --forward-to localhost:3000 --call-id call_abc123- Never expose sensitive data in console logs
- Validate webhook signatures in production
- Use HTTPS for production endpoints
- Implement proper error handling
- Set up monitoring for production webhooks
For production, configure webhooks in the Vapi dashboard:
// Production webhook with signature verification
app.post('/webhook', verifyVapiSignature, async (req, res) => {
// Your production handler
});1. **Verify your server is running** on the specified port
2. **Check the endpoint path** matches your route
3. **Ensure no firewall** is blocking local connections
```bash
# Test your endpoint directly
curl -X POST http://localhost:3000/webhook -d '{}'
```
1. **Check response time** - Vapi expects < 10s response
2. **Avoid blocking operations** in webhook handlers
3. **Use async processing** for heavy operations
```typescript
// Good: Quick response
app.post('/webhook', async (req, res) => {
// Queue for processing
await queue.add('process-webhook', req.body);
res.status(200).send();
});
```
1. **Check CLI authentication** - `vapi auth whoami`
2. **Verify account access** to the resources
3. **Ensure events are enabled** in assistant config
```bash
# Re-authenticate if needed
vapi login
```
```bash
# Development only - skip certificate verification
vapi listen --forward-to https://localhost:3000 --skip-verify
# Or use HTTP for local development
vapi listen --forward-to http://localhost:3000
```
Now that you can test webhooks locally:
- Build webhook handlers: Learn about all webhook events
- Implement tools: Add custom functionality
- Set up production webhooks: Deploy to production
Pro tip: Keep vapi listen running while developing - you'll see all events in real-time and can iterate quickly on your webhook handlers without deployment delays!