sphoroVOICEdocs
Markdown

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

POST/v1/calls
Shell
curl -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"}
  }'
Response202 Accepted
{
  "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.

Always send an 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:

CheckRefusal
The number is suppressed, consent is missing, or it is outside calling hours where the number isA compliance refusal naming which. See compliance.
You are at your concurrency ceilingend_reason: at_capacity. Retry it. See concurrency.
The project is over its monthly budgetend_reason: over_budget. See projects.

You can run the compliance check yourself first, which is what a batch loader should do before enqueuing anything:

GET/v1/calls/compliance/check
Shell
curl -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:

Shell
# 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.csv
Do not fire a thousand calls in a loop with no gap. You will hit your concurrency ceiling within seconds, and every call past it comes back at_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".

RetryDo not retry
no_answer — rang outbad_number — there is no such number
busy — the line was busyrejected — the far end refused outright
at_capacityyour ceiling, not theirsAnything that reached a conversation
not_connected — it never rang; often a route problemcarrier_failure after two attempts — it is not going to start working
voicemail, if you chose hangupAnything 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:

Shell
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.

GET/v1/calls/suppressions
POST/v1/calls/suppressions
Shell
# 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

POST/v1/calls/{id}/end
Shell
curl -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.