Placing calls
One call is one request. A thousand calls is the same request a thousand times, except for the three things that make the difference between a campaign and an incident: the compliance checks, the pacing, and knowing which calls to retry.
One call
/v1/callscurl -s -X POST https://voice.sphoro.com/v1/calls \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "'"$AGENT_ID"'",
"to": "+919876543210",
"call_variables": {"first_name": "Priya", "order_id": "AC-40192"}
}'{
"id": "call_8b21f4c9a07e3d15",
"status": "queued",
"agent_id": "agt_9f2c1a7d4b6e803f",
"to": "+919876543210",
"direction": "outbound",
"created_at": "2026-09-08T09:41:03Z"
}202, not 201: the call has been accepted, not connected. A request
that waited for a phone to be answered would wait thirty seconds and time out in most HTTP
clients. Follow it with webhooks.
Idempotency-Key. Without
one, a retried request — by your code, your load balancer, or a proxy — places a second call to
the same person. With one, the retry returns the original call. This is the cheapest insurance on
this API. See using your API key.What happens before the phone rings
Three refusals, each distinguishable, all of which happen before a carrier is touched:
| Check | Refusal |
|---|---|
| The number is suppressed, consent is missing, or it is outside calling hours where the number is | A compliance refusal naming which. See compliance. |
| You are at your concurrency ceiling | end_reason: at_capacity. Retry
it. See concurrency. |
| The project is over its monthly budget | end_reason: over_budget. See
projects. |
You can run the compliance check yourself first, which is what a batch loader should do before enqueuing anything:
/v1/calls/compliance/checkcurl -s "https://voice.sphoro.com/v1/calls/compliance/check?to=%2B919876543210" \
-H "Authorization: Bearer $SPHORO_API_KEY"A list of calls
There is no batch endpoint that takes a CSV — there is something better, which is a campaign: it paces the dialling, retries on the outcomes you name, can fall back to a message, and can be paused and resumed while it runs.
If you would rather drive it yourself, the loop is simple and the important part is the pacing:
# Deliberately serial, with a gap. See the warning below.
while IFS=, read -r number name; do
curl -s -X POST https://voice.sphoro.com/v1/calls \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Idempotency-Key: campaign-2026-09-08-$number" \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\",\"to\":\"$number\",\"call_variables\":{\"first_name\":\"$name\"}}"
sleep 2
done < contacts.csvat_capacity — so a list that would have completed in an hour instead produces a few
successes and hundreds of failures you now have to reconcile. Deriving the idempotency key from
the campaign and the number, as above, is what makes re-running the loop safe.Which calls to retry
Branch on end_reason, never on status. A call that reached voicemail
and a call that reached your customer both have status: "completed".
| Retry | Do not retry |
|---|---|
no_answer — rang out | bad_number — there is no such
number |
busy — the line was busy | rejected — the far end refused
outright |
at_capacity — your ceiling, not theirs | Anything that reached a conversation |
not_connected — it never rang; often a route
problem | carrier_failure after two attempts — it is not going to
start working |
voicemail, if you chose hangup | Anything where the caller opted out — see below |
The full vocabulary is on transfers and endings.
Retrying without building anything
For "try them again in an hour", the agent can do it itself:
curl -s -X PATCH https://voice.sphoro.com/v1/agents/$AGENT_ID \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"conversation": {"auto_reschedule_seconds": 3600, "auto_reschedule_attempts": 2}}'Anything with branches, a wait window or a fallback channel is a journey.
When somebody asks not to be called
This happens on live calls, and it is handled without you doing anything: a caller who asks
not to be called again is added to your suppression list, the call records
call.opt_out, and every future call to that number is refused before it is
dialled.
/v1/calls/suppressions/v1/calls/suppressions# Add one yourself — from a web form, an email, or a reply to an SMS.
curl -s -X POST https://voice.sphoro.com/v1/calls/suppressions \
-H "Authorization: Bearer $SPHORO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"number": "+919876543210", "reason": "asked by email"}'Read compliance before running a campaign, not after.
Ending a call you placed
/v1/calls/{id}/endcurl -s -X POST https://voice.sphoro.com/v1/calls/$CALL_ID/end \
-H "Authorization: Bearer $SPHORO_API_KEY"Records end_reason: ended_by_api. See
transfers and endings.