Voisnap Docs
API Reference

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

EventDescription
SessionStartedA new conversation session has begun
SessionEndedA session has fully completed and all data is available
ToolCallCompletedAn agent tool (REST API or integration) was called and returned
TransferInitiatedThe agent initiated a call transfer to a human or other number
AnalysisCompletedAI analysis of the session (sentiment, summary, custom fields) is ready
ErrorA 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:

AttemptDelay after previous
1 (initial)Immediate
21 second
32 seconds
44 seconds
58 seconds
616 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

ProviderURL
Twilio SMSPOST https://api.voisnap.ai/webhooks/twilio/sms
WhatsApp (via Twilio)POST https://api.voisnap.ai/webhooks/twilio/whatsapp
TelegramPOST https://api.voisnap.ai/webhooks/telegram/message