CallBooker Lead API
Introduction
Send leads to CallBooker and your AI agent calls them within seconds, qualifies them and books meetings on your calendar. The API lets you add leads, follow their calls and meetings, and get results pushed to your systems with webhooks.
Base URL: https://callbooker.com/api/v1. Requests and responses are JSON (form-encoded requests are accepted too). Dates are ISO 8601 in UTC.
Quickstart
1. Create an API key in Dashboard → Lead API. Pick the agent that will make the calls.
2. Send a lead. The agent starts calling right away.
curl -X POST https://callbooker.com/api/v1/leads \
-H "Authorization: Bearer cb_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sarah Johnson",
"phone": "+14155550142",
"email": "sarah@example.com",
"notes": "Requested a home valuation for a 3-bedroom in Austin"
}'3. Get the results. Add a webhook to receive the call result and booked meetings automatically, or fetch the lead later with GET /leads/:id.
The notes and any extra form answers are given to the agent before the call, so it opens with something relevant (“I saw you asked about a valuation for your 3-bedroom…”).
Authentication
The API, webhooks and MCP are included on the Starter plan and above.
Send your API key in the Authorization header:
Authorization: Bearer cb_live_YOUR_KEYEach key belongs to one agent: every call made with it uses that agent. Create one key per agent or per source (website, CRM) so you can revoke them separately. The X-API-Key header works too.
Keep keys on your server. Never put a key in browser JavaScript or a mobile app: anyone who finds it can place calls on your account. There is no CORS support. From a website form, send the lead from your backend or through Zapier/Make.
Phone numbers
Numbers are validated and stored in international format (E.164, e.g. +14155550142). You can send them in any common format:
- With a country code:
+1 (415) 555-0142,0033 6 12 34 56 78. - Without one, together with
country:"phone": "(415) 555-0142", "country": "US". - Without one, if the key has a default country (set it in the dashboard, next to the key).
A number that isn't valid for its country is rejected with invalid_phone and a message saying why. We never guess a country.
Leads
Don't book meetings? Set the agent's call goal to Qualify & report. It qualifies leads without booking, and the result (interested, callback…), the agent's notes and the answers come back here and in webhooks, so your system can follow up its own way.
A lead is a person your agent calls. Leads sent through the API are filed in the Website Leads list. Sending a phone number you already have updates that lead instead of creating a duplicate.
The lead object
idstringname, phone, email, companystring | nullstatusstringnot-called, interested, callback, booked, not-interested, no-answer, voicemail, do-not-call. Set by the agent after each call.agent_notesstring | nullnotes, fieldsstring | null, objectanswersobject{"budget": "5k", "team_size": "8"}. Set the questions in the agent editor.callback_atstring | nulldo_not_callbooleanupcoming_meetingmeeting | nulllatest_call_id, call_count, last_called_at{
"id": "Xk2vL9qPz3",
"object": "lead",
"name": "Sarah Johnson",
"phone": "+14155550142",
"email": "sarah@example.com",
"company": "Acme Realty",
"status": "interested",
"agent_notes": "Selling in 2 months, wants to compare two agencies.",
"notes": "Requested a home valuation",
"fields": { "budget": "5k", "bedrooms": "3" },
"source": "api",
"do_not_call": false,
"callback_at": null,
"call_count": 1,
"last_called_at": "2026-10-03T14:02:11.000Z",
"latest_call_id": "c9Fh27WkQa",
"upcoming_meeting": null,
"created_at": "2026-10-03T14:01:58.000Z"
}Create a lead
Adds the lead and calls it immediately with the key's agent. Returns the lead with its call.
phonestring, requiredcountrystringnamestringfirst_name + last_name work too. Without a name, the agent greets them with “Hi there”.emailstringcompanystringnotesstringmessage or comments.fieldsobject{"budget": "5k"}. Up to 30. Unknown top-level fields are added here automatically, so you can post a whole form as-is.callbooleantrue. Send false to add the lead without calling.No duplicate calls. If the same number is sent again within 30 minutes (double form submits, retries), you get the existing lead and call back with "duplicate": true and status 200 instead of a second call. New leads return 201.
curl -X POST https://callbooker.com/api/v1/leads \
-H "Authorization: Bearer cb_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sarah Johnson",
"phone": "(415) 555-0142",
"country": "US",
"email": "sarah@example.com",
"company": "Acme Realty",
"notes": "Requested a home valuation",
"fields": { "budget": "5k", "bedrooms": "3" }
}'Retrieve a lead
Returns the lead, including the agent's notes and their upcoming meeting.
curl https://callbooker.com/api/v1/leads/Xk2vL9qPz3 \
-H "Authorization: Bearer cb_live_YOUR_KEY"List leads
Newest first. All leads in your account, from every source.
statusstringsourcestringapi, csv, manual or single-call.created_afterdatelimit, starting_aftercurl "https://callbooker.com/api/v1/leads?status=interested&limit=50" \
-H "Authorization: Bearer cb_live_YOUR_KEY"Call a lead again
Starts a new call to the lead with the key's agent. Returns the call (201). Refused with call_in_progress while they're on a call, and with do_not_call if they asked not to be called.
Calls
The call object
statusstringqueued, in_progress, completed or failed.resultstring | nullbooked, interested, callback, not-interested, do-not-call, voicemail, no-answer, busy, canceled, talked (a real conversation, no outcome recorded), ended (picked up, no conversation) or failed.agent_notesstring | nullanswersobjectduration_secondsnumber | nulltranscriptstring | nullrecording_urlstring | nullmeeting_idstring | nullerrorstring | nullsourcestringapi, single (dashboard) or campaign.{
"id": "c9Fh27WkQa",
"object": "call",
"lead_id": "Xk2vL9qPz3",
"status": "completed",
"result": "booked",
"agent": { "id": "a81kd02", "name": "Sales Agent" },
"to": "+14155550142",
"duration_seconds": 184,
"agent_notes": "Booked a demo for Thursday 3 PM. Team of 5, using spreadsheets today.",
"error": null,
"transcript": "Agent: Hi Sarah, this is Alex from Acme…",
"recording_url": "https://callbooker.com/api/v1/calls/c9Fh27WkQa/recording",
"meeting_id": "mT4ss81Lp",
"source": "api",
"campaign_id": null,
"created_at": "2026-10-03T14:02:11.000Z",
"ended_at": "2026-10-03T14:05:20.000Z"
}Retrieve a call
List calls
Newest first. Filter with lead_id for one lead's calls.
curl "https://callbooker.com/api/v1/calls?lead_id=Xk2vL9qPz3" \
-H "Authorization: Bearer cb_live_YOUR_KEY"Get a recording
Returns the call's audio as an MP3 file. Available on the Starter plan and above, for calls up to 30 days old (90 on Growth, 365 on Scale). Errors: plan_required (403), no_recording (404, for example a call that never connected) and recording_expired (410).
curl "https://callbooker.com/api/v1/calls/c9Fh27WkQa/recording" \
-H "Authorization: Bearer cb_live_YOUR_KEY" \
-o call.mp3Meetings
Meetings your agents booked on your connected calendar (Google Calendar or Calendly).
The meeting object
statusstringbooked, moved (replaced by moved_to) or cancelled.start_time, end_timestringattendedboolean | nullvideo_linkstring | nullinvite_sent_tostring | null{
"id": "mT4ss81Lp",
"object": "meeting",
"lead_id": "Xk2vL9qPz3",
"call_id": "c9Fh27WkQa",
"lead_name": "Sarah Johnson",
"status": "booked",
"start_time": "2026-10-08T19:00:00.000Z",
"end_time": "2026-10-08T19:30:00.000Z",
"duration_minutes": 30,
"time_zone": "America/New_York",
"attended": null,
"video_link": "https://meet.google.com/abc-defg-hij",
"calendar": "google",
"invite_sent_to": "sarah@example.com",
"moved_to": null,
"cancel_reason": null,
"agent": { "id": "a81kd02", "name": "Sales Agent" },
"created_at": "2026-10-03T14:04:52.000Z"
}List meetings
By start time, earliest first. GET /v1/meetings/:id returns one.
from, todatestatusstringlimit, starting_aftercurl "https://callbooker.com/api/v1/meetings?from=2026-10-01&status=booked" \
-H "Authorization: Bearer cb_live_YOUR_KEY"Account
Checks that a key works and shows what it's connected to. Use it as the “test connection” step in no-code tools.
{
"object": "account",
"email": "you@company.com",
"plan": "growth",
"agent": { "id": "a81kd02", "name": "Sales Agent", "phone_number": "+14845550199", "ready_to_call": true },
"minutes": { "used": 312, "limit": 1000, "remaining": 688 },
"key": { "label": "Website form", "default_country": "US" }
}Webhooks
Webhooks send events to your server the moment they happen: a call ends, a meeting is booked. Add an endpoint in Dashboard → Lead API → Webhooks, choose the events, and use Send test event to check it. Every delivery, with your server's answer, is listed there.
Events
call.completedcalllead.status_changedleadprevious_status.meeting.bookedmeetingmeeting.movedmeetingprevious_meeting_id.meeting.cancelledmeetingmeeting.attendedmeetingmeeting.no_showmeetingEvents cover all calls in your account (API, dashboard and campaigns). Use source on the call to tell them apart.
Payload
A POST with a JSON body. data.object is the same object the API returns.
{
"id": "evt_069f25df79c54a62905c5767",
"object": "event",
"type": "call.completed",
"created_at": "2026-10-03T14:05:21.000Z",
"test": false,
"data": {
"object": {
"id": "c9Fh27WkQa",
"object": "call",
"status": "completed",
"result": "booked",
"agent_notes": "Booked a demo for Thursday 3 PM…",
"…": "…"
}
}
}Headers: CallBooker-Event (the type), CallBooker-Delivery (unique per delivery) and CallBooker-Signature.
Verifying signatures
Check that a request really comes from CallBooker. The CallBooker-Signature header looks like t=1759500321,v1=5257a8…. Compute an HMAC-SHA256 of {t}.{raw body} with your endpoint's signing secret (whsec_…) and compare it to v1. Use the raw body exactly as received, before parsing JSON.
import crypto from "crypto";
import express from "express";
const app = express();
const SECRET = process.env.CALLBOOKER_WEBHOOK_SECRET; // whsec_…
app.post("/callbooker/webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("CallBooker-Signature") || "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const valid = v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300; // reject replays
if (!valid || !fresh) return res.status(400).send("Bad signature");
const event = JSON.parse(req.body);
if (event.type === "meeting.booked") {
// e.g. update the deal in your CRM
}
res.sendStatus(200);
});Retries
Answer with any 2xx status within 8 seconds. Do slow work after answering. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, then mark the delivery failed. You can resend any delivery from the dashboard. Retries mean an event can arrive more than once: use the event id to ignore duplicates. Endpoints must be public https:// addresses.
Pagination
List endpoints return { "object": "list", "data": [...], "has_more": true, "next_cursor": "id" }. Use limit (1–100, default 20) and pass next_cursor as starting_after to get the next page.
let cursor = null;
do {
const url = new URL("https://callbooker.com/api/v1/leads");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("starting_after", cursor);
const page = await (await fetch(url, { headers: { Authorization: `Bearer ${key}` } })).json();
for (const lead of page.data) console.log(lead.name, lead.status);
cursor = page.next_cursor;
} while (cursor);Errors
Errors use HTTP status codes and a JSON body that says exactly what to fix:
{
"error": {
"code": "invalid_phone",
"message": "phone \"4155550142\" has no country code. Send it as +14155550142, or pass \"country\" (e.g. \"US\").",
"param": "phone"
}
}400 invalid_request400 invalid_phone401 unauthorized403 plan_required403 agreement_required402 quota_exceeded404 not_found409 do_not_call409 call_in_progress409 agent_not_ready429 rate_limited500 internal_errorEvery request and its error is listed in the dashboard's Request log for 30 days.
Rate limits
60 requests per minute per key (300 on the Scale plan). Every response includes X-RateLimit-Limit and X-RateLimit-Remaining; a 429 includes Retry-After (seconds). Need more? Contact us.
Zapier, Make & forms
No code needed: any tool that can send a web request can send leads, and receive results with webhooks.
Zapier
- Trigger: your form or lead source (Typeform, Webflow, HubSpot, Facebook Lead Ads, Google Sheets…).
- Action: Webhooks by Zapier → POST. URL
https://callbooker.com/api/v1/leads, payload typejson. - Data: map
name,phone,email,notes(andcountryif numbers have no country code). - Headers:
Authorization=Bearer cb_live_YOUR_KEY.
To get results back, create a Zap with Webhooks by Zapier → Catch Hook and add its URL as a webhook in the dashboard.
Make
Add an HTTP → Make a request module: method POST, URL https://callbooker.com/api/v1/leads, header Authorization: Bearer cb_live_YOUR_KEY, body type JSON with the lead's fields. For results, use a Webhooks → Custom webhook trigger and add its URL in the dashboard.
Website forms
Post the form to your own backend and forward it to /v1/leads, or use Zapier/Make. Common field names work as they are (first_name, last_name, phone_number, message…), form-encoded bodies are accepted, and any other answers end up in fields for the agent to use.
Older integrations
The original endpoint POST https://us-central1-callbooker-sdr.cloudfunctions.net/captureLead keeps working with its original response. New integrations should use /v1/leads.