Webhooks

Voicebip delivers real-time events to your webhook URL via HTTP POST with HMAC-SHA256 signatures.

Event Types

Voice

EventDescription
call.initiatedCall started — direction, numbers, agent
call.transcriptionReal-time speech transcript from caller or agent
call.agent_responseText response generated by the AI before TTS synthesis
call.barge_inCaller spoke while the agent was talking — agent interrupted
call.idle_silenceCaller was silent beyond the configured idle timeout
call.qualityPer-call MOS score and RTP stats at call end
call.quality_degradedReal-time quality degradation detected (packet loss, jitter spike)
call.completedCall ended with final duration, transcript, and disposition
tool.invocationAI agent called a custom tool — your webhook must respond with the tool result

SMS & WhatsApp

EventDescription
message.receivedInbound message received from a caller
message.sentOutbound message accepted for delivery
message.deliveredMNO or WhatsApp confirmed message delivery to the handset
message.readWhatsApp recipient opened the conversation and read the message (read receipt)
message.dlrSMS delivery receipt from the MNO with status code
message.failedProvider rejected an outbound message after Voicebip accepted it (e.g. invalid number, blocked content). Payload includes provider error code.
message.send_failedVoicebip itself failed to dispatch an outbound message (e.g. quota exhausted, SMPP session down). Distinct from message.failed, which is a provider-side rejection.
message.opt_outRecipient sent STOP/UNSUBSCRIBE — they are automatically added to the workspace opt-out list
window.closingWhatsApp 24-hour conversation window is closing in 60 minutes

call.barge_in, call.idle_silence, call.quality, and call.quality_degraded can be high-volume on busy agents. If you don’t process them, register a webhook endpoint that filters them out at the edge — every delivered event still counts against your endpoint’s processing capacity.

Internal NATS Subjects (Not Delivered to Webhooks)

Voicebip’s internal pipelines publish additional subjects that are not forwarded to your webhook URL today. They are documented here so you can recognise them in support tickets and dashboard activity feeds, and so the boundary between “webhook events” and “internal pipeline events” is explicit.

SubjectPurpose
billing.low_balanceWorkspace balance dropped below the configured warning threshold. Triggers an email to the workspace owner.
billing.blockedWorkspace balance reached zero. Voice + messaging refuse usage until top-up.
billing.unblockedPaystack or Stripe top-up cleared zero balance. Usage resumes.
billing.pricing.updatedAdmin pricing change. Replicas flush their pricing cache.
usage.recordedPer-event usage line item (one per billable unit).
usage.voice_minutesAggregated voice minute roll-up.
conversation.mode_changedA human agent took over (or released) a messaging conversation; surfaced in the dashboard.
agent.config.invalidatedAn agent’s configuration changed; downstream caches invalidate.
audit.requestAPI request audit trail for compliance.
delivery.dlqDead-letter for webhook deliveries that exhausted all 7 retry attempts. Replay via POST /v1/webhooks/dlq/replay.

If you have a use case for receiving any of these as webhooks, open an issue — most can be surfaced behind a feature flag.

Payload Format

Every webhook delivery is a JSON object with a common outer envelope and an event-type-specific payload field.

Envelope

1{
2 "event_id": "evt_abc123",
3 "event_type": "call.completed",
4 "channel": "voice",
5 "agent_id": "agt_PAEZ_njcfm2kycpjs",
6 "number": "+2349012345678",
7 "from": "+2348031234567",
8 "timestamp": "2026-05-23T12:34:56Z",
9 "payload": { ... }
10}
FieldDescription
event_idStable ID — identical across all retry attempts. Use for idempotency.
event_typeOne of the event types in the table above.
channelvoice, sms, or whatsapp
agent_idThe agent that handled the call or message.
numberYour Voicebip DID (E.164).
fromRemote party number (E.164).
timestampUTC ISO 8601 timestamp.
payloadEvent-specific data — see below.

call.initiated

Published when a call connects and media is flowing. Phone numbers are always E.164; Nigerian DIDs use the +234 country code.

1{
2 "event_id": "evt_abc123",
3 "event_type": "call.initiated",
4 "channel": "voice",
5 "agent_id": "agt_PAEZ_njcfm2kycpjs",
6 "number": "+2349012345678",
7 "from": "+2348031234567",
8 "timestamp": "2026-05-23T12:34:00Z",
9 "payload": {
10 "call_id": "call_01HXYZ...",
11 "workspace_id": "ws_xyz123",
12 "direction": "inbound",
13 "from_number": "+2348031234567",
14 "to_number": "+2349012345678",
15 "mno": "mtn"
16 }
17}
Payload fieldDescription
call_idStable call ID — use this to correlate all subsequent events.
directioninbound (caller rang your number) or outbound (your agent called).
from_numberCaller E.164.
to_numberCalled DID (E.164).
mnoMNO trunk used: mtn, glo, airtel, or 9mobile.

call.transcription

Published on every final STT transcript — once per caller utterance. The turn_id matches the turn_id field in the BYOM webhook request for the same utterance.

1{
2 "event_id": "evt_def456",
3 "event_type": "call.transcription",
4 "channel": "voice",
5 "agent_id": "agt_PAEZ_njcfm2kycpjs",
6 "timestamp": "2026-05-23T12:34:07Z",
7 "payload": {
8 "call_id": "call_01HXYZ...",
9 "turn_id": "turn_xk9j...",
10 "text": "I'd like to check my outstanding balance.",
11 "is_final": true,
12 "confidence": 0.95,
13 "conversation_history": [
14 { "turn_id": "turn_ab12", "role": "agent", "text": "Hi, this is Kemi. How can I help?", "timestamp": "2026-05-23T12:34:02Z" },
15 { "turn_id": "turn_xk9j", "role": "caller", "text": "I'd like to check my outstanding balance.", "timestamp": "2026-05-23T12:34:07Z" }
16 ]
17 }
18}
Payload fieldDescription
turn_idStable ID for this turn. Matches the BYOM request messages[].turn_id.
textTranscribed caller speech.
is_finalAlways true for webhook-delivered events.
confidenceSTT confidence score (0.0–1.0).
conversation_historyRolling history at this point (last 20 turns).

call.completed

Published when the call ends. Contains the full transcript and AI-mode metadata. Check ai_mode to confirm whether the call was handled by your BYOM webhook ("byom") or a hosted provider.

1{
2 "event_id": "evt_ghi789",
3 "event_type": "call.completed",
4 "channel": "voice",
5 "agent_id": "agt_PAEZ_njcfm2kycpjs",
6 "timestamp": "2026-05-23T12:35:02Z",
7 "payload": {
8 "call_id": "call_01HXYZ...",
9 "workspace_id": "ws_xyz123",
10 "status": "completed",
11 "duration_seconds": 62,
12 "direction": "inbound",
13 "mno_used": "mtn",
14 "ai_mode": "byom",
15 "recording_url": "/v1/calls/call_01HXYZ.../recording",
16 "quality_score": 4.1,
17 "transcript": [
18 { "turn_id": "turn_ab12", "role": "agent", "text": "Hi, this is Kemi. How can I help?", "timestamp": "2026-05-23T12:34:02Z" },
19 { "turn_id": "turn_xk9j", "role": "caller", "text": "I'd like to check my outstanding balance.", "timestamp": "2026-05-23T12:34:07Z" },
20 { "turn_id": "turn_mn34", "role": "agent", "text": "Your balance is ₦45,000, due April 20th.", "timestamp": "2026-05-23T12:34:09Z" }
21 ]
22 }
23}
Payload fieldDescription
statusAlways completed.
duration_secondsTotal call length in seconds.
ai_modebyom, hosted, or gemini-live.
recording_urlAPI path to the recording; empty string if recording was off.
quality_scoreMOS score (1.0–5.0) sampled at hangup; 0 when unavailable.
transcriptFull turn-by-turn history (last 20 turns, objects with turn_id, role, text, timestamp).

BYOM agents: lifecycle events (call.initiated, call.transcription, call.completed) are delivered to the same webhook_url as the per-turn BYOM requests — they are not separate URLs. Check for an event_type field in the request body to tell them apart: lifecycle events carry event_type and expect {"received": true}; BYOM turns do not carry event_type and expect a BYOMVoiceWebhookResponse. See the BYOM guide for the turn request/response schema and HMAC verification.

Signature Verification

Every webhook delivery includes the following headers:

HeaderTypeDescription
X-Voicebip-Signaturestringsha256={hex_digest} — HMAC-SHA256 over "{timestamp}.{body}" using your signing secret
X-Voicebip-TimestampstringUnix timestamp (seconds) used in the signature. Read this to reconstruct the signed payload and reject replays older than 5 minutes
X-Voicebip-Event-IDstringStable event ID — identical across all retry attempts. Use for idempotency

The signed payload is "{timestamp}.{body}" — the Unix timestamp string, a literal ., then the raw request body. This lets you reject replayed deliveries by checking the timestamp age.

1import hmac
2import hashlib
3import time
4
5def verify_webhook(body: bytes, headers: dict, secret: str, max_age_seconds: int = 300) -> bool:
6 signature = headers.get("X-Voicebip-Signature", "")
7 timestamp = headers.get("X-Voicebip-Timestamp", "")
8 if not timestamp:
9 return False
10 # Reject replays older than max_age_seconds (default 5 minutes)
11 if abs(time.time() - int(timestamp)) > max_age_seconds:
12 return False
13 signed_payload = f"{timestamp}.{body.decode('utf-8')}".encode()
14 expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
15 return hmac.compare_digest(f"sha256={expected}", signature)
1import (
2 "crypto/hmac"
3 "crypto/sha256"
4 "crypto/subtle"
5 "encoding/hex"
6 "math"
7 "net/http"
8 "strconv"
9 "time"
10)
11
12func verifyWebhook(r *http.Request, body []byte, secret string) bool {
13 signature := r.Header.Get("X-Voicebip-Signature")
14 tsStr := r.Header.Get("X-Voicebip-Timestamp")
15 if tsStr == "" {
16 return false
17 }
18 ts, err := strconv.ParseInt(tsStr, 10, 64)
19 if err != nil {
20 return false
21 }
22 // Reject replays older than 5 minutes
23 skew := time.Since(time.Unix(ts, 0))
24 if skew < 0 {
25 skew = -skew
26 }
27 if skew > 5*time.Minute {
28 return false
29 }
30 mac := hmac.New(sha256.New, []byte(secret))
31 mac.Write([]byte(tsStr))
32 mac.Write([]byte{'.'})
33 mac.Write(body)
34 expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
35 return subtle.ConstantTimeCompare([]byte(expected), []byte(signature)) == 1
36}

Retry Policy

Failed webhook deliveries are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
415 minutes
51 hour
64 hours
724 hours

A delivery is considered failed if your endpoint returns a non-2xx status code or times out after 10 seconds.

Testing Webhooks

POST /v1/webhooks/test delivers a realistic mock payload to any URL with a valid HMAC signature — no real call or message is needed.

$# Simulate a call.completed event
$curl -X POST "https://api.voicebip.com/v1/webhooks/test" \
> -H "Authorization: Bearer pk_live_your_key" \
> -H "Content-Type: application/json" \
> -d '{"webhook_url": "https://your-server.com/webhook", "event_type": "call.completed"}'

The mock payload always includes:

  • Valid Nigerian E.164 phone numbers (+2348031234567, +2349012345678)
  • Realistic conversation history
  • A valid X-Voicebip-Signature HMAC header so your verification code runs

Supported event_type values: call.initiated, call.transcription, call.completed, call.barge_in, call.idle_silence, call.quality, call.quality_degraded, message.received, message.sent, message.delivered, message.failed.

Testing BYOM Routing End-to-End

The fastest way to verify your BYOM webhook before a live call:

  1. Expose a local endpoint with ngrok: ngrok http 8080
  2. Update your agent’s webhook_url to the ngrok URL
  3. Use a pk_test_ API key — sandbox mode synthesizes the full BYOM turn without a real SIP call or billing charge
  4. Fire a call.transcription test event to your events endpoint to see what the event stream looks like alongside the BYOM turn requests:
$curl -X POST "https://api.voicebip.com/v1/webhooks/test" \
> -H "Authorization: Bearer pk_test_your_key" \
> -H "Content-Type: application/json" \
> -d '{
> "webhook_url": "https://your-ngrok.ngrok-free.app/events",
> "event_type": "call.completed"
> }'
  1. Make a sandbox test call (or use the Dashboard’s “Test call” button) and watch both channels: BYOM turn POSTs arrive at webhook_url, lifecycle events arrive at the registered events endpoint.

Signing Secret Rotation

When you rotate your workspace’s signing secret, Voicebip sends both the new and old signatures during a 24-hour grace period:

  • X-Voicebip-Signature — signature using the new secret
  • X-Voicebip-Signature-Previous — signature using the old secret (present during 24h grace window only)

Your verification code should accept either signature during rotation to avoid rejecting valid deliveries.

Multiple Webhook Endpoints (Fan-Out)

Each agent supports multiple webhook URLs. When a matching event fires, the platform delivers the same payload to every active endpoint independently — each has its own attempt counter, retry queue, and delivery status. One slow or failing endpoint does not affect delivery to the others.

Register an Endpoint

$curl -X POST "https://api.voicebip.com/v1/agents/agt_PAEZ_njcfm2kycpjs/webhooks/endpoints" \
> -H "Authorization: Bearer pk_live_your_key" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://primary.example.com/webhook",
> "description": "Primary handler"
> }'

Response:

1{ "id": "whep_abc123def456gh78" }

List Endpoints

$curl "https://api.voicebip.com/v1/agents/agt_PAEZ_njcfm2kycpjs/webhooks/endpoints" \
> -H "Authorization: Bearer pk_live_your_key"
1{
2 "endpoints": [
3 {
4 "id": "whep_abc123def456gh78",
5 "agent_id": "agt_PAEZ_njcfm2kycpjs",
6 "url": "https://primary.example.com/webhook",
7 "active": true,
8 "description": "Primary handler"
9 },
10 {
11 "id": "whep_zyx987wvu654ts32",
12 "agent_id": "agt_PAEZ_njcfm2kycpjs",
13 "url": "https://backup.example.com/webhook",
14 "active": true,
15 "description": "Backup handler"
16 }
17 ]
18}

Pause / Resume an Endpoint

Toggle the active flag without deleting the endpoint:

$curl -X PATCH "https://api.voicebip.com/v1/agents/agt_PAEZ_njcfm2kycpjs/webhooks/endpoints/whep_abc123def456gh78" \
> -H "Authorization: Bearer pk_live_your_key" \
> -H "Content-Type: application/json" \
> -d '{ "active": false }'

Delete an Endpoint

$curl -X DELETE "https://api.voicebip.com/v1/agents/agt_PAEZ_njcfm2kycpjs/webhooks/endpoints/whep_abc123def456gh78" \
> -H "Authorization: Bearer pk_live_your_key"

The legacy webhook_url field on the agent object is the backward-compatible single-endpoint configuration. It remains the fallback destination when no rows exist in webhook_endpoints. Both mechanisms can coexist — if an agent has a webhook_url and registered endpoints, the consumer uses the registered endpoints and treats webhook_url as a belt-and-braces fallback only when no active endpoint rows are found.

Webhook Delivery Log

Monitor recent webhook deliveries via the API or the Voicebip Dashboard.

List Deliveries

$curl "https://api.voicebip.com/v1/webhooks/deliveries?page_size=20" \
> -H "Authorization: Bearer pk_live_your_key"

Each delivery entry includes the event type and payload, HTTP status code from your endpoint, retry attempt count, and next retry timestamp if applicable.

Get a Single Delivery

$curl "https://api.voicebip.com/v1/webhooks/deliveries/del_abc123" \
> -H "Authorization: Bearer pk_live_your_key"

Replay Failed Deliveries (DLQ)

If your endpoint was down during the retry window, you can replay failed deliveries from the dead-letter queue:

$curl -X POST "https://api.voicebip.com/v1/webhooks/dlq/replay" \
> -H "Authorization: Bearer pk_live_your_key" \
> -H "Content-Type: application/json" \
> -d '{"event_ids": ["evt_abc123", "evt_def456"]}'

Replayed deliveries restart the full 7-attempt retry sequence from attempt 1.