Receive real-time HTTP callbacks when events occur in your AIVO account — calls, appointments, contacts, and messages.
Calls, appointments, contacts, SMS
Verify every request is authentic
3 retries with exponential backoff
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
Configure webhooks from your AIVO dashboard:
https://your-app.com/webhooks/aivo)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.
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 |
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: ed25519:<base64-signature>
X-AIVO-Timestamp: 1711699200
X-AIVO-Event: call.completed
User-Agent: AIVO-Webhooks/1.0call.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": "maria@example.com",
"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"
}
}Always verify webhook signatures
Without verification, an attacker could send fake events to your endpoint. AIVO signs every webhook with Ed25519.
Every webhook request includes an X-AIVO-Signature header containing an Ed25519 signature, and an X-AIVO-Timestamp header with the Unix timestamp.
Verification steps:
X-AIVO-Timestamp and X-AIVO-Signature headers${timestamp}.${rawBody}import crypto from "node:crypto";
/**
* Verify an AIVO webhook signature.
*
* @param rawBody - The raw request body string (do NOT parse JSON first)
* @param signature - Value of X-AIVO-Signature header (without "ed25519:" prefix)
* @param timestamp - Value of X-AIVO-Timestamp header
* @param publicKey - Your webhook's Ed25519 public key (base64, from dashboard)
* @returns true if signature is valid and timestamp is fresh
*/
function verifyAivoWebhook(
rawBody: string,
signature: string,
timestamp: string,
publicKey: 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. Verify the Ed25519 signature
const keyObject = crypto.createPublicKey({
key: Buffer.from(publicKey, "base64"),
format: "der",
type: "spki",
});
return crypto.verify(
null, // Ed25519 doesn't use a separate hash algorithm
Buffer.from(payload),
keyObject,
Buffer.from(signature, "base64")
);
}
// — Usage in an Express/Next.js route handler —
import { NextRequest, NextResponse } from "next/server";
const WEBHOOK_PUBLIC_KEY = "your-webhook-public-key"; // Replace with your actual key
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") ?? "";
// Strip "ed25519:" prefix if present
const signature = sigHeader.replace("ed25519:", "");
if (!verifyAivoWebhook(rawBody, signature, timestamp, WEBHOOK_PUBLIC_KEY)) {
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 });
}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.
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.