BeastCaller API documentation
Push leads into your BeastCaller campaigns from your own systems and keep them up to date. Outbound webhooks and Quick Actions send data the other way. The API is small on purpose: today it covers lead intake.
Base URL
https://api.beastcaller.app/ext/v1Endpoints
| Endpoint | What it does |
|---|---|
| POST /leads | Create one or more leads in a campaign |
| PATCH /leads/{externalId} | Update a lead by your own external ID |
Requests and responses are JSON over HTTPS (request bodies up to 2 MB). Call the API from your server or an automation tool such as Zapier, Make or n8n — never from browser code, where your key would be visible.
Authentication
Send your workspace API key in the X-Api-Key header with every request:
X-Api-Key: ck_your_api_key- A key belongs to one workspace and only reaches that workspace’s campaigns and leads.
- Keys start with
ck_followed by 40 hexadecimal characters. A key is shown once when it is created; BeastCaller only stores a hash of it. - A missing key or an unknown key returns
401. - Treat the key like a password: keep it on your server or in your tool’s secret store.
Getting a key: keys are created by a workspace owner or admin. There is no self-serve API key screen in the app yet — email support@beastcaller.app from your owner or admin account and we will help you set one up.
Rate limits
Each API key can make 100 requests per minute. Send leads in batches rather than one request per lead. Responses carry these headers:
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests allowed per minute (100) |
| X-RateLimit-Remaining | Requests left in the current window |
| X-RateLimit-Reset | When the window resets (Unix time, seconds) |
Over the limit, the API answers 429 with a Retry-After header (seconds):
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"error": "Too Many Requests",
"message": "Rate limit exceeded. Try again in 42 seconds.",
"retryAfter": 42
}Create leads
/ext/v1/leadsAdds one or more leads to a campaign in your workspace. New leads appear in the campaign’s queue in the phase given by status (Queued by default).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| campaignId | string | Yes | ID of a campaign in your workspace. It is the last part of the campaign page URL (/campaigns/{id}). |
| leads | array | Yes | One or more lead objects (see below). An empty array is rejected. |
Lead object
| Field | Type | Required | Description |
|---|---|---|---|
| phone | string | Yes | Stored as sent (whitespace trimmed). Use international E.164 format, e.g. +4520123456, so the dialer can call it. |
| name | string | No | Contact name. |
| company | string | No | Company name. |
| city | string | No | City. |
| niche | string | No | Industry or segment label. |
| externalId | string | No | Your own ID for the lead (e.g. your CRM record ID). Needed to update it later. |
| status | string | No | Where the lead starts in the campaign. See the status values table. Default: Queued. |
Other fields are ignored.
Status values
| status | Campaign phase |
|---|---|
| queued, new | Queued |
| attempting, attempting_contact, in_progress, retry | Attempting |
| connected | Connected |
| follow_up, follow-up, follow_up_required | Follow Up |
| not_interested, invalid, do_not_call, skipped | Not Interested |
| closed, completed | Closed |
| anything else, or no status | Queued |
If the campaign has no matching phase, the lead goes to Queued.
Example request
curl -X POST https://api.beastcaller.app/ext/v1/leads \
-H "X-Api-Key: ck_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "your_campaign_id",
"leads": [
{
"externalId": "crm-1042",
"phone": "+4520123456",
"name": "Jane Doe",
"company": "Acme ApS",
"city": "Aarhus",
"niche": "Solar"
}
]
}'Response
HTTP/1.1 201 Created
{
"inserted": 1
}insertedis the number of leads in your request.- Leads are not de-duplicated: sending the same lead twice creates it twice.
- If any lead in the batch has no
phonestring, the whole request is rejected and nothing is created. - Each new lead triggers a
lead_createdwebhook (see Outbound webhooks).
Update a lead
/ext/v1/leads/{externalId}Updates the lead(s) in your workspace with the given externalId — the ID you sent when you created the lead. Send at least one of the fields below.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | No | Moves the lead to the matching campaign phase (same values as for creating leads). Unrecognised values move the lead to Queued. |
| meta | any JSON | No | Stored on the lead as-is. Replaces any meta value stored before. |
Example request
curl -X PATCH https://api.beastcaller.app/ext/v1/leads/crm-1042 \
-H "X-Api-Key: ck_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"status": "follow_up",
"meta": { "dealStage": "proposal-sent" }
}'Response
HTTP/1.1 200 OK
{
"updated": 1
}updatedis the number of leads changed. Every lead in your workspace with thatexternalIdis updated.- A successful update triggers a
lead_updatedwebhook.
Errors
| Status | When |
|---|---|
| 401 | The X-Api-Key header is missing, or the key is unknown. |
| 429 | The key made more than 100 requests in the current minute. |
| 500 | The request was rejected by the endpoint — for example a missing campaignId, an empty leads array, a lead without phone, a campaign that is not in your workspace, nothing to update, or an unknown externalId — or an unexpected server error. |
Validation and lookup problems currently come back with status 500 as well, not 400/404. Read the error message in the body: it says what to fix.
401 response
HTTP/1.1 401 Unauthorized
{
"ok": false,
"error": {
"code": "auth.unauthenticated",
"message": "Invalid API key",
"correlationId": "…"
},
"status": "fail",
"message": "Invalid API key"
}Endpoint error response
HTTP/1.1 500 Internal Server Error
{
"error": "campaignId is required"
}Outbound webhooks
Get a POST request in your own system when leads are created or updated through the API. A workspace owner or admin adds an HTTPS endpoint under Workspace Settings → Integrations → Custom Webhook and picks the events to send. All selected events go to the same URL.
Events
| Event | Sent when |
|---|---|
| lead_created | Once per lead created through POST /ext/v1/leads |
| lead_updated | After a lead is updated through PATCH /ext/v1/leads/{externalId} |
The webhook settings also list Call Ended and Campaign Finished. Those events are not triggered by normal use of the app yet (calls in the call console, pausing or resuming campaigns), so do not build on them. For automation during a call, use Quick Actions.
Delivery
- JSON
POSTwith the headersX-BeastCaller-Event(the event name) andX-BeastCaller-Signature: the hex HMAC-SHA256 of the raw body, keyed with your workspace’s webhook secret. The secret is not shown in the app yet — email support@beastcaller.app if you want to verify signatures. - Answer with any
2xxstatus. Redirects are not followed. - Failed deliveries are retried automatically. After repeated failures a delivery is marked failed; you can see every delivery, and replay it, in the Webhook Deliveries log of the integration settings.
timestampis when the event was sent. Lead fields you did not provide arenull.
lead_created
POST https://your-endpoint.example.com/beastcaller
Content-Type: application/json
X-BeastCaller-Event: lead_created
X-BeastCaller-Signature: <hex HMAC-SHA256 of the body>
{
"event": "lead_created",
"workspaceId": "…",
"timestamp": "2026-09-29T10:15:00.000Z",
"lead": {
"phone": "+4520123456",
"name": "Jane Doe",
"company": "Acme ApS",
"city": "Aarhus",
"niche": "Solar",
"externalId": "crm-1042",
"externalSource": "api"
}
}lead_updated
changes contains what the update wrote: phaseId (the internal ID of the new phase) and/or meta.
{
"event": "lead_updated",
"workspaceId": "…",
"timestamp": "2026-09-29T10:20:00.000Z",
"lead": {
"externalId": "crm-1042",
"changes": {
"phaseId": "…",
"meta": { "dealStage": "proposal-sent" }
}
}
}Quick Actions
Quick Actions are one-click buttons in the call console. When a caller clicks one, BeastCaller POSTs the current lead to your HTTPS endpoint — for example to send a contract, book a meeting, update your CRM or start a Zapier or Make scenario.
- Owners and admins set them up per campaign under Campaign settings → Quick Actions: a label, your HTTPS URL and a signing secret you choose (at least 16 characters). Up to 10 per workspace.
- Reply with a
2xxwithin 5 seconds. If your endpoint times out or fails, the run is queued and retried in the background for up to 24 hours. Retried requests carry"retry": trueand include only the lead’sid, so make your endpoint safe to call more than once. - The same action runs at most 5 times per lead per hour. A repeat click shortly after a successful run does not send a second request.
- The Test button sends
quick_action.testwith"test": trueand no lead.
Request headers
| Header | Value |
|---|---|
| X-Beastcaller-Event | quick_action.execute (or quick_action.test) |
| X-Beastcaller-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with your signing secret |
| X-Beastcaller-Timestamp | Send time in milliseconds since the Unix epoch |
| X-Beastcaller-Execution-Id | ID of this run (quick_action.execute only) |
| User-Agent | Beastcaller-Webhook/1.0 |
Example request
email and fieldOverrides are only included when the caller filled in fields in the action’s form.
POST https://your-endpoint.example.com/send-contract
Content-Type: application/json
User-Agent: Beastcaller-Webhook/1.0
X-Beastcaller-Event: quick_action.execute
X-Beastcaller-Execution-Id: …
X-Beastcaller-Timestamp: 1790676900000
X-Beastcaller-Signature: sha256=<hex HMAC-SHA256 of the body>
{
"event": "quick_action.execute",
"action": { "id": "…", "label": "Send contract" },
"lead": {
"id": "…",
"externalId": "crm-1042",
"name": "Jane Doe",
"phone": "+4520123456",
"company": "Acme ApS",
"email": "jane@acme.example"
},
"fieldOverrides": { "email": "jane@acme.example" },
"executionId": "…",
"workspaceId": "…",
"timestamp": 1790676900000
}Verifying signatures
Compute the HMAC over the exact raw body you received (before parsing JSON) and compare in constant time.
import crypto from "node:crypto";
// rawBody: the exact request body as received (string or Buffer).
// Quick Actions send "sha256=<hex>"; workspace webhooks send the bare hex.
export function isValidSignature(rawBody, signatureHeader, secret) {
const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = String(signatureHeader || "").replace(/^sha256=/, "");
const a = Buffer.from(digest);
const b = Buffer.from(received);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Questions about the API?
Email support@beastcaller.app. New to BeastCaller? Start with a 14-day free trial, no credit card required.