Register an endpoint
Dashboard: Settings → API & MCP → Webhook endpoints → Add endpoint.
API: POST /api/v1/locations/:id/webhook-endpoints.
You provide a label, an https:// URL, and the list of events you care about. PickupBell returns an HMAC signing secret on creation — store it. Subsequent GET calls omit the secret.
curl -X POST https://api.pickupbell.com/api/v1/locations/premier-hvac/webhook-endpoints \
-H "Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000" \
-H "content-type: application/json" \
-d '{
"label": "production CRM sync",
"url": "https://api.yourcrm.com/webhooks/pickupbell",
"events": ["call.ended", "lead.created", "booking.created"]
}'
# → { "data": { "id": "…", "secret": "whsec_…", ... } }
Event types
| Type | When it fires |
|---|
call.ended | Every call, including spam-rejected. One per call. |
call.emergency | Caller hit urgency 5 or the agent warm-transferred. |
lead.created | A call produced a lead row (outcome is lead/booked/transferred/voicemail). |
lead.updated | Lead status changed (new → contacted, etc.). |
booking.created | The AI scheduled an appointment on a calendar. |
booking.updated | Booking status or time changed. |
spam.blocked | Spam call rejected before the agent answered. |
A single event can fire multiple types (e.g. an emergency call triggers call.ended and call.emergency and lead.created). Subscribe only to what you need.
Delivery shape
We POST JSON to your endpoint:
{
"event": "lead.created",
"location_id": "5e9a7b21-…",
"created_at": "2026-04-22T14:47:52Z",
"data": {
"id": "8a3f9d21-…",
"call_id": "…",
"name": "Sarah Chen",
"phone": "+15125550142",
"service_type": "AC repair",
"urgency_level": 5,
"estimated_value": 2800,
"status": "new"
}
}
Headers on every delivery:
content-type: application/json
user-agent: PickupBell-Webhooks/1.0
pickupbell-event: lead.created
pickupbell-event-id: 4a7…
pickupbell-delivery-id: 8f9…
pickupbell-timestamp: 1713879084
pickupbell-signature: v1=<hex hmac-sha256 of the raw body using your secret>
Verifying signatures
Every request carries pickupbell-signature: v1=<hex> — HMAC-SHA256 of the raw request body using your endpoint’s secret.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(req: Request, secret: string): boolean {
const sig = (req.headers.get("pickupbell-signature") ?? "").replace(/^v1=/, "");
const body = (req as unknown as { rawBody?: string }).rawBody ?? "";
const expected = createHmac("sha256", secret).update(body).digest("hex");
return (
sig.length === expected.length &&
timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))
);
}
Always verify against the raw request body, not a re-serialized parsed object. Different key ordering or whitespace in your re-serialization will break the signature check.
Idempotency
Each delivery has a unique pickupbell-delivery-id. On retry (see below), the delivery-id changes but the pickupbell-event-id stays stable — use event-id to dedupe in your consumer.
Retries
If your endpoint returns a non-2xx status or times out (10s), we retry with exponential backoff: 1m → 5m → 30m → 2h → 6h → 24h. After 6 failed attempts the delivery is marked dead and we stop. The endpoint’s failure_count counter tracks consecutive failures; a successful delivery resets it.
To inspect history, GET /api/v1/locations/:id/webhook-deliveries returns the most recent delivery attempts with status codes and response bodies.
Pausing without revoking
PATCH /webhook-endpoints/:id with { "active": false } pauses delivery (pending retries are not re-enqueued until you flip active back to true). DELETE permanently removes the endpoint and discards pending deliveries.