POST /v1/suppression-list), and never holds more live calls than your project’s live-call limit.
Campaigns are dialled by a separate process,
turncall-campaign-worker — the same image as the API with a different entrypoint. A campaign runs for hours, and the API’s event loop is busy carrying live calls. Run at least one worker wherever campaigns should dial; several can run side by side.Create a campaign
variablesreach the agent the way call-init variables do:{{name}}in the system prompt, first message and tool descriptions is replaced with the target’s value. A target takes up to 50 variables and 4,096 characters of them, names included; more is a422.timezoneis required for every target — on the target itself, or as the campaign’s default. It must be an IANA name such asAmerica/New_York. A target with neither is refused (422). TurnCall never guesses a zone from the number: a ported mobile keeps its area code wherever its owner lives, and the target’s local time is what calling hours are read in.- A number listed twice is refused: it would call the same person twice. Up to 10,000 targets per request.
A/B test agents
Instead ofagent_id, give agents — two to ten agents with weights that sum to 100 — and the targets are split between them:
agent_id; each call’s agent_id and call.ended say who called. Send exactly one of agent_id and agents.
The response is the campaign, queued until the worker reaches it, with its targets counted by status.
Calling windows
calling_window is the hours a target may be called, in that target’s own timezone: with {"start": "09:00", "end": "20:00"}, a New York target is called between 09:00 and 20:00 New York time and a London target between 09:00 and 20:00 London time, whatever the clock says where the worker runs. Daylight saving is applied on the day itself, so the window does not drift by an hour when a zone changes.
startis inclusive andendexclusive, asHH:MM, with no offset — the zone is always the target’s.startmust be beforeend; a window across midnight is refused rather than guessed at.days(mon…sun) limits it to those days of the target’s week. Leave it out for every day.- A target whose turn comes outside its window goes back to
pending, and itsnext_attempt_atsays when its window next opens. - With no
calling_window, targets are called at any hour.
Retries
retry_policy says which endings are dialled again, how many times and how far apart. Its keys are the call’s ended_reason:
Each takes
max_attempts — dials to the target in all, the first one included, at most 10 — and spacing_seconds, how long after the call ended to try again (at least 60). A call that ends in a listed way puts its target back to pending with next_attempt_at set, until its attempts reach that ending’s max_attempts. attempts counts every dial whatever its ending, so a target that went unanswered once and was then busy has used two of busy’s attempts.
A retry respects the calling window: one that comes due after hours waits for the window’s next opening. A retry that has come due is dialled before targets not yet tried, so its spacing is kept even in a long list. Every other ending — the person answered, the call was transferred, the carrier failed — is final. With no retry_policy, each target is dialled once.
Follow it
Each call also carries
campaign_id and campaign_target_id in its metadata, so call.ended webhooks tie back to the target. The campaign moves from running to completed once no target is left to dial.
Events
Webhook subscribers (POST /v1/webhooks) get three campaign events, each at most once:
The payload is the campaign as
GET /v1/campaigns/{id} returns it — status, agent or weights, target counts per status, timestamps — and the envelope carries campaign_id (null on every other event). Per-target results are on GET /v1/campaigns/{id}/targets.
Pause, resume, cancel
pausestops new dials. Calls already placed stay up, and their endings are still recorded on their targets. The campaign ispauseduntil you resume it.resumecarries on where it stopped.cancelends the campaign for good: it becomescancelled, and so does every target not yet dialled. Calls already placed finish normally.
409 invalid_state_transition. A pause or cancel that lands while a worker is about to dial a target still stops that dial.
If a worker crashes
Nothing is lost, and nobody is called twice. A worker that dies mid-dial leaves its targetdialling or placing, and the next worker to run picks it up once the claim is older than CAMPAIGN_CLAIM_TIMEOUT_SECONDS (default 120):
dialling— the carrier was never asked, so the target goes back topendingand is dialled.placing— the carrier may have placed the call before the worker could record it, so the target is endedfailedwitherrorstartingdial_outcome_unknown, and is not dialled again. Check the carrier’s call log for that number before adding it to a new campaign.
How it paces
Every dial — a first attempt or a retry — goes through the same path asPOST /v1/calls/outbound, and a campaign is never refused by your limits: before each dial the worker reserves the call’s live-call slot and its place in the number’s calls-per-second window, so several workers — or a worker and your own API dials — never race each other into a 429.
- At
live_calls, the campaign waits and picks up again as calls end. - At
outbound_calls_per_second, it comes back the moment the number’s one-second window frees: a number limited to 5 dials five a second, not one per poll. - With no rate set, a campaign dials one call a second from each number (
CAMPAIGN_DEFAULT_CALLS_PER_SECOND, default1— Twilio’s default rate for an account, past which Twilio queues your calls and they ring late). Setoutbound_calls_per_secondto go faster, or0on that variable to turn the floor off.
live_calls limit, how many calls run at once is the pace times how long your calls last — set one if your agent’s backends have a ceiling.