Webhooks
Configure outbound event webhooks, verify signatures, and set up inbound provider webhooks for Twilio, Vonage, and Telnyx.
Webhooks
Voisnap uses webhooks to notify your server of events happening in the platform. You configure a URL on your agent and Voisnap sends HTTP POST requests as events occur.
Base path: /api/v1/webhooks
Outbound agent webhooks
Event types
| Event | Description |
|---|---|
SessionStarted | A new conversation session has begun |
SessionEnded | A session has fully completed and all data is available |
ToolCallCompleted | An agent tool (REST API or integration) was called and returned |
TransferInitiated | The agent initiated a call transfer to a human or other number |
AnalysisCompleted | AI analysis of the session (sentiment, summary, custom fields) is ready |
Error | A non-recoverable error occurred during the session |
Configure a webhook
POST /api/v1/webhooks
curl -X POST https://api.voisnap.ai/api/v1/webhooks \
-H "Authorization: Bearer vsnp_live_..." \
-H "Content-Type: application/json" \
-d '{
"agentId": "agt_01HXK8Z3MNPQRS",
"url": "https://your-server.com/webhooks/voisnap",
"events": ["SessionStarted", "SessionEnded", "AnalysisCompleted"],
"secret": "whsec_your_signing_secret_here",
"enabled": true
}'
List webhooks
GET /api/v1/webhooks
Get a webhook
GET /api/v1/webhooks/{webhookId}
Update a webhook
PATCH /api/v1/webhooks/{webhookId}
Delete a webhook
DELETE /api/v1/webhooks/{webhookId}
Event payload schemas
All events share a common envelope:
{
"id": "evt_01HXMN8ZABC123",
"type": "SessionEnded",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:25:47Z",
"data": { ... }
}
SessionStarted
{
"id": "evt_01HX...",
"type": "SessionStarted",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:22:00Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"channel": "phone",
"direction": "inbound",
"caller": { "phoneNumber": "+14155550199", "country": "US" },
"metadata": { "customerId": "cust_12345" }
}
}
SessionEnded
{
"id": "evt_01HX...",
"type": "SessionEnded",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:25:47Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"channel": "phone",
"direction": "inbound",
"durationSeconds": 227,
"endReason": "user_ended",
"caller": { "phoneNumber": "+14155550199", "country": "US" },
"cost": {
"totalUsd": 0.094,
"llmUsd": 0.041,
"ttsUsd": 0.028,
"sttUsd": 0.018,
"telephonyUsd": 0.007
},
"metadata": { "customerId": "cust_12345" }
}
}
ToolCallCompleted
{
"id": "evt_01HX...",
"type": "ToolCallCompleted",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:23:15Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"toolName": "lookup_customer",
"toolType": "rest_api",
"input": { "phone_number": "+14155550199" },
"output": { "customer_id": "cust_12345", "name": "Jane Smith", "tier": "premium" },
"latencyMs": 234,
"success": true
}
}
TransferInitiated
{
"id": "evt_01HX...",
"type": "TransferInitiated",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:24:30Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"transferTo": "+14155550101",
"transferType": "warm",
"reason": "customer_requested_human",
"summaryForAgent": "Customer calling about billing dispute on invoice INV-2025-0612."
}
}
AnalysisCompleted
{
"id": "evt_01HX...",
"type": "AnalysisCompleted",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:26:15Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"sentiment": "positive",
"sentimentScore": 0.81,
"intent": "billing_inquiry",
"summary": "Customer called about a charge on their June invoice. Issue resolved.",
"customFields": {
"resolution_status": "resolved",
"issue_category": "billing"
}
}
}
Error
{
"id": "evt_01HX...",
"type": "Error",
"agentId": "agt_01HXK8Z3MNPQRS",
"tenantId": "ten_01HXABC...",
"timestamp": "2025-06-16T14:22:45Z",
"data": {
"conversationId": "conv_01HXDEF456GHI",
"errorCode": "STT_PROVIDER_TIMEOUT",
"errorMessage": "Deepgram transcription timed out after 5000ms",
"recoverable": false
}
}
Signature verification
Every webhook delivery includes three security headers:
X-Webhook-Signature: sha256=hmac-sha256-hex-digest
X-Webhook-Timestamp: 1718547600
X-Webhook-Delivery-Id: evt_01HXMN8ZABC123
The signature is HMAC-SHA256(secret, timestamp + "." + raw_body).
:::warning Always verify the webhook signature before processing the payload. Reject requests where the timestamp is more than 5 minutes old (clock skew protection). :::
Verification examples
import hmac
import hashlib
import time
from fastapi import Request, HTTPException
WEBHOOK_SECRET = "whsec_your_signing_secret_here"
async def verify_webhook(request: Request) -> bytes:
signature = request.headers.get("X-Webhook-Signature", "")
timestamp = request.headers.get("X-Webhook-Timestamp", "")
body = await request.body()
# Check timestamp freshness (5-minute window)
if abs(time.time() - int(timestamp)) > 300:
raise HTTPException(status_code=400, detail="Webhook timestamp too old")
# Compute expected signature
signed_content = f"{timestamp}.{body.decode()}"
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(),
signed_content.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise HTTPException(status_code=401, detail="Invalid webhook signature")
return body
import crypto from 'crypto';
import type { Request, Response } from 'express';
const WEBHOOK_SECRET = 'whsec_your_signing_secret_here';
export function verifyWebhook(req: Request, res: Response): Buffer | null {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const body = req.body as Buffer; // use raw body middleware
// Check timestamp freshness
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
res.status(400).json({ error: 'Webhook timestamp too old' });
return null;
}
const signedContent = `${timestamp}.${body.toString()}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedContent)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
res.status(401).json({ error: 'Invalid signature' });
return null;
}
return body;
}
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"strconv"
"time"
)
func Verify(secret, signature, timestamp string, body []byte) error {
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp")
}
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return fmt.Errorf("timestamp too old")
}
signed := fmt.Sprintf("%s.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signed))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(expected), []byte(signature)) {
return fmt.Errorf("invalid signature")
}
return nil
}
require 'openssl'
WEBHOOK_SECRET = 'whsec_your_signing_secret_here'
def verify_webhook(signature, timestamp, body)
raise 'Timestamp too old' if (Time.now.to_i - timestamp.to_i).abs > 300
signed_content = "#{timestamp}.#{body}"
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, signed_content)
raise 'Invalid signature' unless Rack::Utils.secure_compare(expected, signature)
end
Retry policy
If your endpoint returns a non-2xx status code or times out (>5 seconds), Voisnap retries delivery:
| Attempt | Delay after previous |
|---|---|
| 1 (initial) | Immediate |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
| 6 | 16 seconds |
After 6 failed attempts the event is marked as permanently failed. You can view failed deliveries in the Console under Webhooks → Delivery Log and manually replay them.
:::tip
Use the X-Webhook-Delivery-Id header as an idempotency key. Store processed delivery IDs and skip duplicates in case retries occur.
:::
Inbound provider webhooks
Voisnap automatically handles inbound webhooks from your telephony providers. You don't configure these yourself — Voisnap sets the correct callback URLs when you provision numbers via the API.
Twilio voice webhook
POST https://api.voisnap.ai/webhooks/twilio/voice
Vonage voice webhook
POST https://api.voisnap.ai/webhooks/vonage/voice/answer
POST https://api.voisnap.ai/webhooks/vonage/voice/event
Telnyx voice webhook
POST https://api.voisnap.ai/webhooks/telnyx/voice
SMS / Messaging webhooks
| Provider | URL |
|---|---|
| Twilio SMS | POST https://api.voisnap.ai/webhooks/twilio/sms |
| WhatsApp (via Twilio) | POST https://api.voisnap.ai/webhooks/twilio/whatsapp |
| Telegram | POST https://api.voisnap.ai/webhooks/telegram/message |