Skip to content
BookifyCall
Developer docs: Webhooks

Webhooks

We send an HTTPS POST to your server when something happens in your account: a call ends, its AI summary is ready, a lead or appointment is created. Every delivery is signed and retried until your server accepts it.

Subscribe

Add an endpoint in the app, or with the API (scope webhooks:write). The URL must be public https. The response contains the endpoint's signing secret, shown once; you can also send your own (16 to 256 characters).

curl -X POST "https://bookifycall.com/api/v1/external/webhooks" \
  -H "X-API-Key: $BOOKIFYCALL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"CRM sync","url":"https://crm.example.com/hooks/bookifycall","events":["call.completed","call.analyzed","lead.created"]}'

Manage subscriptions with GET, PATCH and DELETE /webhooks (reference). Webhooks are part of the Enterprise plan.

What a delivery looks like

A JSON body with a unique event id, the event name, when it happened, your organization id and the event data. Headers:

  • X-Webhook-Signature: sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your endpoint secret.
  • X-Webhook-Id
  • X-Webhook-Event
  • User-Agent: BookifyCall-Webhooks/1.0
Example delivery: lead.created
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "lead.created",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+15551230001",
    "company": "Example Dental",
    "status": "qualified",
    "score": 72,
    "temperature": "hot",
    "source_call_id": "00000000-0000-4000-8000-000000000001",
    "created_at": "2026-01-15T16:34:10.000Z",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}

Verify the signature

Compute the HMAC over the raw request body and compare in constant time. Reject anything that does not match, then deduplicate on the event id: a retry of the same event carries the same id.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.BOOKIFYCALL_WEBHOOK_SECRET;

// Verify against the raw bytes: re-serialised JSON would not match the signature.
app.post('/hooks/bookifycall', express.raw({ type: 'application/json' }), async (req, res) => {
  const expected =
    'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const given = req.get('X-Webhook-Signature') ?? '';
  const valid =
    given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString('utf8'));
  if (await alreadyProcessed(event.id)) return res.status(200).end(); // retries reuse the id
  await enqueue(event); // do the work after answering
  res.status(200).end();
});

Retries, ordering and replay

  • Answer with any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure. Do slow work after answering.
  • A failed delivery is retried after 1 min, 5 min, 30 min, 2 h, 8 h (6 attempts in all, about 10.5 hours).
  • An endpoint is switched off only after a delivery used every retry and nothing reached it for 24 hours. Events keep being recorded meanwhile.
  • After an outage, turn the endpoint back on (PATCH /webhooks/:id with { "enabled": true }) and resend what it missed with POST /webhooks/replay (since a date, 500 per call).
  • Deliveries run in parallel and can arrive out of order. Every record carries updated_at: keep the newest.

Events

16 events. Payload fields are snake_case and only ever added to, never renamed or removed. call.completed is sent when the call ends, before the AI analysis exists; call.analyzed follows with the summary, usually seconds later. Join them on call_id. All values below are examples.

EventSent when
call.startedAn AI call started (inbound or outbound)
call.completedA call ended normally; includes the transcript and recording link. The AI summary arrives later in call.analyzed
call.failedA call failed or was not answered
call.transferredThe AI handed a live call to a person
message.takenThe AI took a message for your team
lead.createdA new lead was created
lead.status_changedA lead's status changed
appointment.bookedAn appointment was booked
appointment.confirmedAn appointment was confirmed
appointment.cancelledAn appointment was cancelled
scheduled_call.createdA callback/outbound call was scheduled
scheduled_call.completedA scheduled call was placed
scheduled_call.failedA scheduled call gave up after its retries
call.analyzedThe AI summary of a call is ready: summary, outcome, intent, sentiment and caller details
appointment.rescheduledAn appointment moved to a new time
call.recording_readyThe audio recording of a call is stored and can be fetched from the recording endpoint
call.startedAn AI call started (inbound or outbound)
Example delivery: call.started
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.started",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "connected",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": null,
    "duration_seconds": null,
    "end_reason": null,
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
call.completedA call ended normally; includes the transcript and recording link. The AI summary arrives later in call.analyzed
Example delivery: call.completed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.completed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "ended",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": "2026-01-15T16:34:10.000Z",
    "duration_seconds": 250,
    "end_reason": "caller_hangup",
    "summary": null,
    "topics": [
      "pricing"
    ],
    "updated_at": "2026-01-15T16:34:10.000Z",
    "call_sid": "CA00000000000000000000000000000000",
    "outcome": null,
    "transcript": [
      {
        "speaker": "agent",
        "text": "Thanks for calling Example Dental, how can I help?",
        "at": "2026-01-15T16:30:00.000Z"
      },
      {
        "speaker": "user",
        "text": "I would like to book a demo.",
        "at": "2026-01-15T16:30:05.000Z"
      }
    ],
    "transcript_truncated": false,
    "transcript_entry_count": 2,
    "recording": {
      "url": "https://recordings.example.com/calls/example.mp3?X-Amz-Expires=3600&X-Amz-Signature=example",
      "expires_in_seconds": 3600,
      "duration_seconds": 250
    },
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "appointment_id": "00000000-0000-4000-8000-000000000004",
    "ai_provenance": {
      "prompt_id": "agent:00000000-0000-4000-8000-000000000002:inbound",
      "prompt_version": "3f9a1c0e",
      "provider": "openai",
      "model": "gpt-realtime",
      "voice_mode": "openai_native"
    }
  }
}
call.failedA call failed or was not answered
Example delivery: call.failed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.failed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "failed",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": "2026-01-15T16:34:10.000Z",
    "duration_seconds": 0,
    "end_reason": "no_answer",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
call.transferredThe AI handed a live call to a person
Example delivery: call.transferred
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.transferred",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "transferring",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": "2026-01-15T16:34:10.000Z",
    "duration_seconds": 250,
    "end_reason": "caller_hangup",
    "updated_at": "2026-01-15T16:34:10.000Z",
    "transfer_to": "+15551230003",
    "contact_name": "Front desk",
    "reason": "Caller asked for a person"
  }
}
message.takenThe AI took a message for your team
Example delivery: message.taken
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "message.taken",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "ended",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": "2026-01-15T16:34:10.000Z",
    "duration_seconds": 250,
    "end_reason": "caller_hangup",
    "updated_at": "2026-01-15T16:34:10.000Z",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "caller_name": "Jane Doe",
    "callback_number": "+15551230001",
    "message": "Please call back about rescheduling.",
    "urgency": "normal",
    "taken_at": "2026-01-15T16:34:10.000Z"
  }
}
lead.createdA new lead was created
Example delivery: lead.created
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "lead.created",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+15551230001",
    "company": "Example Dental",
    "status": "qualified",
    "score": 72,
    "temperature": "hot",
    "source_call_id": "00000000-0000-4000-8000-000000000001",
    "created_at": "2026-01-15T16:34:10.000Z",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
lead.status_changedA lead's status changed
Example delivery: lead.status_changed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "lead.status_changed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": "+15551230001",
    "company": "Example Dental",
    "status": "qualified",
    "score": 72,
    "temperature": "hot",
    "source_call_id": "00000000-0000-4000-8000-000000000001",
    "created_at": "2026-01-15T16:34:10.000Z",
    "updated_at": "2026-01-15T16:34:10.000Z",
    "previous_status": "new"
  }
}
appointment.bookedAn appointment was booked
Example delivery: appointment.booked
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "appointment.booked",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "appointment_id": "00000000-0000-4000-8000-000000000004",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "client_company": "Example Dental",
    "meeting_purpose": "Product demo",
    "scheduled_at": "2026-01-20T15:00:00.000Z",
    "end_at": "2026-01-20T15:30:00.000Z",
    "timezone": "America/New_York",
    "duration_minutes": 30,
    "status": "scheduled",
    "confirmed": true,
    "meet_link": "https://meet.google.com/abc-defg-hij",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
appointment.confirmedAn appointment was confirmed
Example delivery: appointment.confirmed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "appointment.confirmed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "appointment_id": "00000000-0000-4000-8000-000000000004",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "client_company": "Example Dental",
    "meeting_purpose": "Product demo",
    "scheduled_at": "2026-01-20T15:00:00.000Z",
    "end_at": "2026-01-20T15:30:00.000Z",
    "timezone": "America/New_York",
    "duration_minutes": 30,
    "status": "scheduled",
    "confirmed": true,
    "meet_link": "https://meet.google.com/abc-defg-hij",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
appointment.cancelledAn appointment was cancelled
Example delivery: appointment.cancelled
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "appointment.cancelled",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "appointment_id": "00000000-0000-4000-8000-000000000004",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "client_company": "Example Dental",
    "meeting_purpose": "Product demo",
    "scheduled_at": "2026-01-20T15:00:00.000Z",
    "end_at": "2026-01-20T15:30:00.000Z",
    "timezone": "America/New_York",
    "duration_minutes": 30,
    "status": "cancelled",
    "confirmed": true,
    "meet_link": "https://meet.google.com/abc-defg-hij",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
scheduled_call.createdA callback/outbound call was scheduled
Example delivery: scheduled_call.created
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "scheduled_call.created",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "scheduled_call_id": "00000000-0000-4000-8000-000000000005",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "scheduled_at": "2026-10-20T15:00:00.000Z",
    "timezone": "America/New_York",
    "reason": "Follow up on the demo request",
    "status": "pending",
    "attempts": 0,
    "call_id": null,
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
scheduled_call.completedA scheduled call was placed
Example delivery: scheduled_call.completed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "scheduled_call.completed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "scheduled_call_id": "00000000-0000-4000-8000-000000000005",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "scheduled_at": "2026-10-20T15:00:00.000Z",
    "timezone": "America/New_York",
    "reason": "Follow up on the demo request",
    "status": "completed",
    "attempts": 1,
    "call_id": "00000000-0000-4000-8000-000000000001",
    "updated_at": "2026-01-15T16:34:10.000Z"
  }
}
scheduled_call.failedA scheduled call gave up after its retries
Example delivery: scheduled_call.failed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "scheduled_call.failed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "scheduled_call_id": "00000000-0000-4000-8000-000000000005",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "scheduled_at": "2026-10-20T15:00:00.000Z",
    "timezone": "America/New_York",
    "reason": "Follow up on the demo request",
    "status": "failed",
    "attempts": 3,
    "call_id": null,
    "updated_at": "2026-01-15T16:34:10.000Z",
    "failure_reason": "no_answer"
  }
}
call.analyzedThe AI summary of a call is ready: summary, outcome, intent, sentiment and caller details
Example delivery: call.analyzed
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.analyzed",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "direction": "inbound",
    "status": "ended",
    "from": "+15551230001",
    "to": "+15551230002",
    "started_at": "2026-01-15T16:30:00.000Z",
    "ended_at": "2026-01-15T16:34:10.000Z",
    "duration_seconds": 250,
    "end_reason": "caller_hangup",
    "updated_at": "2026-01-15T16:34:10.000Z",
    "call_sid": "CA00000000000000000000000000000000",
    "summary": "Caller asked about pricing and booked a demo for Tuesday.",
    "outcome": "appointment_booked",
    "intent": "book_demo",
    "intent_description": "Wants a product demo",
    "sentiment": "positive",
    "satisfaction_score": 4,
    "topics": [
      "pricing"
    ],
    "key_points": [
      "Team of 12",
      "Wants a demo"
    ],
    "follow_up": {
      "needed": true,
      "suggested_action": "Send pricing sheet",
      "due_hint": "before the demo"
    },
    "caller": {
      "name": "Jane Doe",
      "company": "Example Dental",
      "email": "jane@example.com",
      "callback_number": "+15551230001"
    },
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "analyzed_at": "2026-01-15T16:34:30.000Z"
  }
}
appointment.rescheduledAn appointment moved to a new time
Example delivery: appointment.rescheduled
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "appointment.rescheduled",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "appointment_id": "00000000-0000-4000-8000-000000000004",
    "lead_id": "00000000-0000-4000-8000-000000000003",
    "agent_id": "00000000-0000-4000-8000-000000000002",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "client_company": "Example Dental",
    "meeting_purpose": "Product demo",
    "scheduled_at": "2026-01-20T15:00:00.000Z",
    "end_at": "2026-01-20T15:30:00.000Z",
    "timezone": "America/New_York",
    "duration_minutes": 30,
    "status": "scheduled",
    "confirmed": true,
    "meet_link": "https://meet.google.com/abc-defg-hij",
    "updated_at": "2026-01-15T16:34:10.000Z",
    "previous_scheduled_at": "2026-01-19T15:00:00.000Z"
  }
}
call.recording_readyThe audio recording of a call is stored and can be fetched from the recording endpoint
Example delivery: call.recording_ready
{
  "id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ0",
  "event": "call.recording_ready",
  "timestamp": "2026-01-15T16:34:10.000Z",
  "organization_id": "00000000-0000-4000-8000-000000000100",
  "data": {
    "call_id": "00000000-0000-4000-8000-000000000001",
    "recording_id": "00000000-0000-4000-8000-000000000007",
    "duration_seconds": 250,
    "format": "mp3",
    "stored": true,
    "recording_endpoint": "/api/v1/external/calls-history/00000000-0000-4000-8000-000000000001/recording"
  }
}

Coming from the previous BookifyCall API?

Subscriptions that use a previous event name (call_completed, lead_captured, appointment_booked, lead.captured) keep receiving the previous camelCase format with a bare hex signature, so existing receivers work unchanged. Register with the names above to switch to the current format.