Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.pickupbell.com/llms.txt

Use this file to discover all available pages before exploring further.

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

TypeWhen it fires
call.endedEvery call, including spam-rejected. One per call.
call.emergencyCaller hit urgency 5 or the agent warm-transferred.
lead.createdA call produced a lead row (outcome is lead/booked/transferred/voicemail).
lead.updatedLead status changed (new → contacted, etc.).
booking.createdThe AI scheduled an appointment on a calendar.
booking.updatedBooking status or time changed.
spam.blockedSpam 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.