Concurrency
How many calls your account carries at once, what happens to the ones that
arrive after that, and why an outbound list has to be paced rather than fired. Worth reading
before a campaign, not after it produces a wall of at_capacity.
Two numbers, not one
Your account has a ceiling on calls that are talking, and a second, separate one on calls that are waiting. Past both, a caller is turned away immediately.
arriving call
│
├─ a slot is free ─────────────▶ admitted the greeting, as always
├─ full, room in the queue ────▶ held the hold line, on repeat
└─ full, and the queue is full ▶ turned away the busy line, then hangupWhy an inbound call cannot simply be "queued"
The caller is already connected. There is no buffer to put them in — "queued" can only mean "answered and held", and that is not free: the carrier leg is up and billing.
What holding does save is the expensive part. A held call has no speech recogniser, no model and no synthesis behind it, which is where both the money and the memory go. So holding trades a little carrier spend for a lot of headroom, which is the trade a contact-centre queue has always made.
Fairness
Waiting calls are queued per account and admitted round-robin, and one account may hold at most half the queue plus one. Without that, a single runaway campaign fills a shared queue and every other account's callers wait behind it — one customer's incident becomes everybody's.
Outbound: pacing, not queueing
An outbound call that does not fit is not held, because nobody is waiting on the line yet.
It simply fails with end_reason: at_capacity and is yours to retry.
POST /v1/calls with no gap fills the
ceiling in seconds; everything after that comes back at_capacity, and you are left
reconciling hundreds of failures that were never dialled. Nothing was wrong with the numbers, the
agent, or the carrier.Two ways to avoid it:
| Approach | What it does |
|---|---|
| A campaign | Paces the dialling for you, against your own ceiling, and can be paused and resumed. This is the answer for a list of any size. |
| Your own loop with a gap | Fine for tens of calls. Derive the
Idempotency-Key from the list and the number so re-running it is safe. See
placing calls. |
What one call costs in capacity
| Kind of call | Takes a slot |
|---|---|
| A conversation agent, inbound or outbound | Yes — recogniser, model and voice |
A keypad agent | Yes, but a much cheaper one: nothing in a turn is a network call |
A normal agent | Only until it transfers. After the handover the call is between the caller and the person. |
| A held inbound call | A queue place, not a talking slot |
A webrtc call | Effectively nothing after the introduction — the audio runs directly between the two clients |
Knowing where you are
Both of these read the same window; the first is the number that matters, the second is the list of calls behind it.
# How many calls did not go out because you were full?
curl -s "https://voice.sphoro.com/v1/analytics/overview?from=2026-09-01&to=2026-09-08" \
-H "Authorization: Bearer $SPHORO_API_KEY" \
| jq '.end_reasons.at_capacity'
# Which ones were they?
curl -s "https://voice.sphoro.com/v1/calls?limit=100" \
-H "Authorization: Bearer $SPHORO_API_KEY" \
| jq '[.data[] | select(.end_reason == "at_capacity") | .to]'Concurrency shows up two ways: as at_capacity on individual calls, and as a
number in analytics. The second is the one to watch — a rising
count of at_capacity over a week is a ceiling you have grown into, not a bad
afternoon.
at_capacity
count is climbing, or a campaign needs headroom it does not have,
tell us before the campaign rather than during
it.