Outbound campaigns
Upload a list, and an agent calls everyone on it through your own telephony account. Paced, retried sensibly, and confined to the hours the law allows.
Calling hours are enforced, not documented
How it works
- We POST to your provider: dial this number, and when it is answered fetch this URL.
- The person answers.
- Your provider fetches our URL and we return XML telling it to open a websocket.
- The call runs on the same engine as everything else.
Step 3 is the security-sensitive one. A provider cannot authenticate — it is a machine fetching a URL — so that endpoint is public, and whatever is in the URL is effectively public too. It therefore carries a single-use call token, valid for minutes, that grants permission to start exactly one call. An API key in that position would put full account access in every provider's request log. A replayed token is refused.
Uploading a list
curl -X POST https://api.latency.cam/v1/contact-lists \
-H "Authorization: Bearer $LATENCY_KEY" -H "Content-Type: application/json" \
-d '{"name": "September deliveries", "csv": "name,phone,order_id\nAsim,+91 90000 00011,ORD-1"}'The phone column is found by name — number, phone, mobile, msisdn and others all work — because requiring an exact header means a customer's export fails for a reason they cannot see. Every other column becomes a variable, so a file with name and order_id columns produces an agent that can say both without any mapping.
Duplicates are collapsed rather than rejected. A list assembled from two exports routinely contains the same person twice, and refusing the whole file is unhelpful when the right behaviour — call them once — is obvious. Since numbers are matched on their last ten digits, +91…, 91… and 0… are recognised as one person.
Starting a campaign
curl -X POST https://api.latency.cam/v1/campaigns \
-H "Authorization: Bearer $LATENCY_KEY" -H "Content-Type: application/json" \
-d '{
"name": "Delivery reminders",
"agent_id": "ag_...",
"list_id": "cl_...",
"account_id": "pa_...",
"concurrency": 3,
"attempts_max": 2,
"retry_after_s": 1800,
"start": true
}'The agent's version is pinned at creation, for the same reason a live call pins one: a campaign running for two days must not change what it says because somebody edited the agent halfway through.
The four rules the dialer follows
| Rule | Why it is this way |
|---|---|
| Nobody is called twice | Every attempt is claimed from the database atomically, so two ticks of the loop — or two processes — cannot take the same contact. This is the worst bug available here, because its symptom is a person's phone ringing over and over. |
| Do-not-call is checked at dial time | Not at upload. Somebody who asks not to be called during a campaign has to stop being called by that campaign, and a list filtered at import cannot honour that. |
| A bad credential stops the campaign | It would fail identically for all ten thousand contacts, so retrying is a slower way to fail while spending your money. A busy line is retried; a refused credential is not. |
| Concurrency is capped at 6 | One GPU serves about eleven conversations. Outbound is the lowest-priority traffic on the box and must leave room for the inbound calls that are somebody's business. |
Do not call
curl -X POST https://api.latency.cam/v1/dnc \
-H "Authorization: Bearer $LATENCY_KEY" -H "Content-Type: application/json" \
-d '{"numbers": ["+919876543210"], "reason": "asked not to be called"}'Rotating the number you call from
Carriers and handset spam filters score a number by how much it calls. One number placing three thousand calls in a morning gets flagged and quietly stops connecting; six numbers sharing that load do not. You experience the difference as an answer rate that collapses for no visible reason, and the natural thing to blame is the agent.
curl -X PUT https://api.latency.cam/v1/provider-accounts/pa_3d81/numbers \
-H "Authorization: Bearer $LATENCY_KEY" -H "Content-Type: application/json" \
-d '{"numbers": ["+918000000001", "+918000000002", "+918000000003"]}'Every attempt records the number it went out on, so afterwards you can answer “which of my numbers stopped connecting” rather than guessing. Setting the list replaces it rather than merging, because the number you most want to remove is the one that just got blocked.
One call, without a campaign
Building a contact list and a campaign to make a single call is four steps of setup for one conversation, and a customer who cannot cheaply test an agent ships it untested. It is also the wrong shape for a call your own software triggers — an order shipped, a payment failed.
curl -X POST https://api.latency.cam/v1/calls/outbound \
-H "Authorization: Bearer $LATENCY_KEY" -H "Content-Type: application/json" \
-d '{
"to_number": "+919876543210",
"agent_id": "ag_7f3a2b",
"account_id": "pa_3d81",
"variables": {"name": "Asim", "order_id": "A-1029"}
}'409. There is no call record until somebody answers, because until then nothing was said.Watching it run
| Path | Gives |
|---|---|
GET /v1/campaigns | All of them, with progress and outcomes |
GET /v1/campaigns/{id} | One, plus whether the calling window is open right now |
GET /v1/campaigns/{id}/calls | Every attempt, its outcome, and a link to the transcript |
POST /v1/campaigns/{id}/pause | Stop new calls. Conversations in progress finish |
POST /v1/campaigns/{id}/cancel | Stop entirely |
Pausing deliberately does not hang up on the person currently talking. You pressed pause to stop reaching new people.
It survives a restart
All pacing state lives in the database, so a deploy in the middle of a campaign resumes instead of re-dialling everybody, and attempts that were in flight when the process stopped are returned to the queue rather than sitting marked “dialing” forever, which would stop the campaign ever completing.