Connect your CRM
The loop most integrations run: new leads go out as a campaign, outcomes come back to the record.
Most integrations are one scheduled job that does four things. This page shows the shape of it; the quickstart has runnable code for each call.
Using HubSpot, Zoho CRM, Pipedrive or Google Sheets? The dashboard's Connectors import contacts and log calls back without any code. The API is for everything else.
The loop
every 15 minutes:
1. leads = your CRM's new leads that haven't been called
2. POST /campaigns/json one campaign for the batch, crm_id in each contact's extra
3. POST /campaigns/{id}/launch start dialing
4. mark those leads "calling" in your CRM
on each call.ended webhook (or every 5 minutes, polling GET /calls?since=...):
5. write status, outcome and summary to the record in contact.extra.crm_id1. Pick the agent once
Agents rarely change, so look the id up once (or create the agent from code, see Managing agents) and keep it in your configuration.
2. Create a campaign per batch
Group the leads you want to call now into one campaign. Name it so you can find it in the dashboard, for example by date and source.
{
"name": "Website leads 2026-10-05 14:00",
"agent_id": "8b8e3307-2c1a-4f6e-9a51-0d7c2b9e4f10",
"campaign_prompt": "They filled the pricing form. Offer a demo this week.",
"contacts": [
{ "phone_number": "+919876543210", "name": "Asha", "extra": { "crm_id": "42", "source": "pricing-form" } }
]
}- Every number must be E.164 (
+91…). One bad row rejects the whole request and names the row, so clean numbers before sending. - Up to 10,000 contacts per campaign.
extratakes any JSON. Put whatever you need to find the record again.
3. Launch
POST /campaigns/{campaign_id}/launch dials every pending contact. If you'd rather a person
approve each batch, stop after step 2 and have them press Launch in the dashboard. A key with
calls:write but no calls:dispatch can't launch, which enforces this.
4. Write results back
Register a webhook for call.ended and update the record in
contact.extra as each call finishes. If you'd rather not run a public endpoint, poll
GET /calls instead: Syncing results covers doing that without missing
or double-processing calls.
What each field is good for in a CRM:
| Field | Use it for |
|---|---|
status, end_reason | Whether the call connected and how it ended. |
outcome | A short label set by the agent, such as requirements_gathered. Good for pipeline stages. |
summary | A sentence or two for the activity note. |
duration_seconds | Call length, for reporting. |
transcript (from GET /calls/{call_id}) | The full conversation, for the record or for review. |
recording (from GET /calls/{call_id}/recording-url) | Fetch the link when someone wants to listen. It expires after an hour, so don't store it. |
Keys for this integration
Give the job its own key with agents:read, calls:write, calls:dispatch and calls:read,
and set a daily dispatch cap a little above your normal volume. If something loops, the cap stops
it before it spends your balance.