Skip to content
Back to Home

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:

  1. Navigate to Settings → Webhooks in your dashboard
  2. Click Add Webhook Endpoint
  3. Enter your HTTPS endpoint URL (e.g. https://your-app.com/webhooks/aivo)
  4. Select which events you want to receive, or choose All Events
  5. 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.

EventDescriptionCategory
call.completedFired when a call ends normally
call.missedFired when an inbound call goes unanswered or is abandoned
call.transferredFired when a call is transferred to another number or agent
appointment.createdFired when the AI agent books a new appointment
appointment.updatedFired when an existing appointment is rescheduled
appointment.cancelledFired when an appointment is cancelled
contact.createdFired when a new contact is added to your account
sms.receivedFired when an inbound SMS/MMS arrives on your AIVO number
sms.sentFired 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 string
  • timestamp — ISO 8601 timestamp of the event
  • idempotencyKey — Unique key for deduplication (see best practices)
  • data — The event payload (varies by event type)

Additionally, each request includes these headers:

Webhook request 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.0

Example Payloads

call.completed

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

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

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

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

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

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

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

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

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 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:

  1. Extract the X-AIVO-Timestamp and X-AIVO-Signature headers
  2. Construct the signed payload: ${timestamp}.${rawBody}
  3. Verify the Ed25519 signature using your webhook's public key (from Settings → Webhooks)
  4. Reject requests older than 5 minutes to prevent replay attacks
Node.js — Ed25519 signature verification
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 });
}

Retry Policy

If your endpoint doesn't respond with a 2xx status code within 10 seconds, AIVO will retry delivery using exponential backoff:

AttemptDelayApprox. Time After First Attempt
1st retry30 seconds~30s
2nd retry5 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.