Webhooks
Receive real-time HTTP callbacks when events occur in your AIVO account - calls, appointments, contacts, and messages.
9 Event Types
Calls, appointments, contacts, SMS
Ed25519 Signed
Verify every request is authentic
Auto Retry
3 retries with exponential backoff
What Are Webhooks?
Webhooks are HTTP callbacks that AIVO sends to your server when events happen in your account. Instead of polling our API for updates, you register a URL and we push data to you in real time.
When your AI voice agent completes a call, books an appointment, or receives an SMS, AIVO makes an HTTP POST request to your configured endpoint with a JSON payload describing the event.
Common use cases
- Sync new contacts and appointments to your CRM
- Send follow-up emails after completed calls
- Trigger workflows in Zapier, Make, or n8n
- Log call analytics to your data warehouse
- Alert your team when calls are missed or transferred
Configuration
Configure webhooks from your AIVO dashboard:
- Navigate to Settings → Webhooks in your dashboard
- Click Add Webhook Endpoint
- Enter your HTTPS endpoint URL (e.g.
https://your-app.com/webhooks/aivo) - Select which events you want to receive, or choose All Events
- Click Save - AIVO will send a test ping to verify your endpoint
Signing key
After creating your webhook, you'll see an Ed25519 public key. Copy this and store it securely - you'll need it to verify incoming webhook signatures.
You can register up to 5 webhook endpoints per account. Each endpoint can subscribe to different event types. All endpoints must use HTTPS.
Event Types
AIVO sends the following event types. Subscribe only to the events you need to minimize unnecessary traffic.
| Event | Description | Category |
|---|---|---|
call.completed | Fired when a call ends normally | |
call.missed | Fired when an inbound call goes unanswered or is abandoned | |
call.transferred | Fired when a call is transferred to another number or agent | |
appointment.created | Fired when the AI agent books a new appointment | |
appointment.updated | Fired when an existing appointment is rescheduled | |
appointment.cancelled | Fired when an appointment is cancelled | |
contact.created | Fired when a new contact is added to your account | |
sms.received | Fired when an inbound SMS/MMS arrives on your AIVO number | |
sms.sent | Fired when an outbound SMS/MMS is delivered |
Payload Format
All webhook payloads are sent as application/json HTTP POST requests. Every payload includes:
event- The event type stringtimestamp- ISO 8601 timestamp of the eventidempotencyKey— Unique key for deduplication (see best practices)data- The event payload (varies by event type)
Additionally, each request includes these headers:
POST https://your-app.com/webhooks/aivo
Content-Type: application/json
X-AIVO-Signature: sha256=<hex-hmac>
X-AIVO-Timestamp: 1711699200
X-AIVO-Event: call.completed
User-Agent: AIVO-Webhooks/1.0Example Payloads
call.completed
{
"event": "call.completed",
"timestamp": "2026-03-29T12:00:00Z",
"idempotencyKey": "evt_abc123def456",
"data": {
"id": "call_01JQXYZ...",
"direction": "inbound",
"from": "+5012279446",
"to": "+5012001234",
"durationSeconds": 142,
"status": "completed",
"summary": "Customer asked about hours and booked Tuesday appointment.",
"recordingUrl": "https://aivo.bz/recordings/call_01JQXYZ.mp3",
"aiConfidenceScore": 0.94,
"startedAt": "2026-03-29T11:57:38Z",
"endedAt": "2026-03-29T12:00:00Z"
}
}call.missed
{
"event": "call.missed",
"timestamp": "2026-03-29T13:05:00Z",
"idempotencyKey": "evt_ghi789jkl012",
"data": {
"id": "call_01JQABC...",
"direction": "inbound",
"from": "+5016001234",
"to": "+5012001234",
"durationSeconds": 0,
"status": "missed",
"reason": "no_answer",
"startedAt": "2026-03-29T13:04:30Z"
}
}call.transferred
{
"event": "call.transferred",
"timestamp": "2026-03-29T14:22:10Z",
"idempotencyKey": "evt_mno345pqr678",
"data": {
"id": "call_01JQDEF...",
"direction": "inbound",
"from": "+5012279446",
"to": "+5012001234",
"transferredTo": "+5016009999",
"transferReason": "customer_request",
"durationBeforeTransfer": 45,
"startedAt": "2026-03-29T14:21:25Z"
}
}appointment.created
{
"event": "appointment.created",
"timestamp": "2026-03-29T12:01:00Z",
"idempotencyKey": "evt_stu901vwx234",
"data": {
"id": "apt_01JQXYZ...",
"contactId": "ctc_01JQABC...",
"contactName": "Maria Santos",
"contactPhone": "+5016123456",
"startsAt": "2026-04-01T10:00:00Z",
"endsAt": "2026-04-01T10:30:00Z",
"service": "General Consultation",
"notes": "Booked by AI during inbound call",
"createdAt": "2026-03-29T12:01:00Z"
}
}appointment.updated
{
"event": "appointment.updated",
"timestamp": "2026-03-30T09:15:00Z",
"idempotencyKey": "evt_yza567bcd890",
"data": {
"id": "apt_01JQXYZ...",
"contactId": "ctc_01JQABC...",
"contactName": "Maria Santos",
"previousStartsAt": "2026-04-01T10:00:00Z",
"startsAt": "2026-04-02T14:00:00Z",
"endsAt": "2026-04-02T14:30:00Z",
"service": "General Consultation",
"updatedAt": "2026-03-30T09:15:00Z"
}
}appointment.cancelled
{
"event": "appointment.cancelled",
"timestamp": "2026-03-30T16:00:00Z",
"idempotencyKey": "evt_efg123hij456",
"data": {
"id": "apt_01JQXYZ...",
"contactId": "ctc_01JQABC...",
"contactName": "Maria Santos",
"startsAt": "2026-04-02T14:00:00Z",
"cancelledBy": "contact",
"reason": "Schedule conflict",
"cancelledAt": "2026-03-30T16:00:00Z"
}
}contact.created
{
"event": "contact.created",
"timestamp": "2026-03-29T12:01:00Z",
"idempotencyKey": "evt_klm789nop012",
"data": {
"id": "ctc_01JQABC...",
"name": "Maria Santos",
"phone": "+5016123456",
"email": "[email protected]",
"source": "ai_call",
"createdAt": "2026-03-29T12:01:00Z"
}
}sms.received
{
"event": "sms.received",
"timestamp": "2026-03-29T15:30:00Z",
"idempotencyKey": "evt_qrs345tuv678",
"data": {
"id": "msg_01JQXYZ...",
"from": "+5016001234",
"to": "+5012001234",
"body": "Can I reschedule my Tuesday appointment?",
"type": "sms",
"direction": "inbound",
"receivedAt": "2026-03-29T15:30:00Z"
}
}sms.sent
{
"event": "sms.sent",
"timestamp": "2026-03-29T15:31:00Z",
"idempotencyKey": "evt_wxy901zab234",
"data": {
"id": "msg_01JQABC...",
"from": "+5012001234",
"to": "+5016001234",
"body": "Your appointment has been rescheduled to Wednesday at 2 PM.",
"type": "sms",
"direction": "outbound",
"status": "delivered",
"sentAt": "2026-03-29T15:31:00Z"
}
}Signature Verification
Always verify webhook signatures
Without verification, an attacker could send fake events to your endpoint. AIVO signs every webhook with HMAC-SHA256.
Every webhook request includes an X-AIVO-Signature header containing an HMAC-SHA256 signature, and an X-AIVO-Timestamp header with the Unix timestamp.
Verification steps:
- Extract the
X-AIVO-TimestampandX-AIVO-Signatureheaders - Construct the signed payload:
${timestamp}.${rawBody} - Compute HMAC-SHA256 of the signed payload using your webhook secret (from Settings → Webhooks)
- Compare the computed signature with the header value (constant-time comparison)
- Reject requests older than 5 minutes to prevent replay attacks
import crypto from "node:crypto";
/**
* Verify an AIVO webhook signature.
*
* @param rawBody - The raw request body string (do NOT parse JSON first)
* @param sigHeader - Value of X-AIVO-Signature header (e.g. "sha256=abc123...")
* @param timestamp - Value of X-AIVO-Timestamp header
* @param secret - Your webhook's signing secret (from dashboard)
* @returns true if signature is valid and timestamp is fresh
*/
function verifyAivoWebhook(
rawBody: string,
sigHeader: string,
timestamp: string,
secret: string
): boolean {
// 1. Reject stale timestamps (5-minute window)
const ageSeconds = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
if (ageSeconds > 300) {
console.warn("Webhook timestamp too old:", ageSeconds, "seconds");
return false;
}
// 2. Construct the signed payload
const payload = `${timestamp}.${rawBody}`;
// 3. Compute expected HMAC-SHA256
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
// 4. Strip "sha256=" prefix
const received = sigHeader.replace("sha256=", "");
// 5. Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received, "hex")
);
}
// - Usage in an Express/Next.js route handler —
import { NextRequest, NextResponse } from "next/server";
const WEBHOOK_SECRET = process.env.AIVO_WEBHOOK_SECRET!;
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const sigHeader = req.headers.get("X-AIVO-Signature") ?? "";
const timestamp = req.headers.get("X-AIVO-Timestamp") ?? "";
if (!verifyAivoWebhook(rawBody, sigHeader, timestamp, WEBHOOK_SECRET)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON.parse(rawBody);
console.log("Verified webhook:", event.event, event.idempotencyKey);
// Process the event asynchronously (e.g., push to a queue)
// await queue.push(event);
// Respond with 200 quickly
return NextResponse.json({ received: true });
}Retry Policy
If your endpoint doesn't respond with a 2xx status code within 10 seconds, AIVO will retry delivery using exponential backoff:
| Attempt | Delay | Approx. Time After First Attempt |
|---|---|---|
| 1st retry | 30 seconds | ~30s |
| 2nd retry | 5 minutes | ~5m 30s |
| 3rd retry (final) | 30 minutes | ~35m 30s |
After 3 failed retries, the event is marked as failed. You can view failed deliveries and manually retry them from Settings → Webhooks → Delivery Log.
Automatic disabling
If your endpoint fails consistently for 24 hours, AIVO will automatically disable the webhook and send you an email notification. Re-enable it from the dashboard once your endpoint is healthy.
Best Practices
Respond with 2xx quickly
Return a 200 or 202 response within a few seconds. If your processing takes longer, push the event to a queue and process it asynchronously. AIVO times out after 10 seconds.
Process asynchronously
Don't perform heavy work (database writes, API calls, email sending) in the webhook handler itself. Acknowledge receipt, push to a background queue (Redis, SQS, Bull, etc.), and process later.
Use idempotency keys
Every webhook includes an idempotencyKey field. Store processed keys and skip duplicates - retries may deliver the same event more than once. A simple approach is to use a Redis SET or a database unique constraint on the key.
Verify signatures
Always validate the X-AIVO-Signature header before processing any webhook. See the signature verification section above.
Use HTTPS endpoints
Webhook URLs must use HTTPS. AIVO will not deliver events to plain HTTP endpoints. Use a service like ngrok for local development.
Monitor delivery health
Check the Delivery Log in your dashboard periodically. Failed deliveries often indicate endpoint bugs, timeouts, or network issues that need attention.