# 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**

```bash
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**

```json
{
  "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](https://voice.sphoro.com/docs/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](https://voice.sphoro.com/docs/authentication).

## 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](https://voice.sphoro.com/docs/compliance). |
| You are at your concurrency ceiling | `end_reason: at_capacity`. Retry it. See [concurrency](https://voice.sphoro.com/docs/capacity). |
| The project is over its monthly budget | `end_reason: over_budget`. See [projects](https://voice.sphoro.com/docs/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**

```bash
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](https://voice.sphoro.com/docs/campaigns): 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**

```bash
# 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"`.

| 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](https://voice.sphoro.com/docs/transfers).

## Retrying without building anything

For "try them again in an hour", the agent can do it itself:

**Shell**

```bash
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](https://voice.sphoro.com/docs/journeys).

## 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**

```bash
# 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](https://voice.sphoro.com/docs/compliance) before running a campaign, not after.

## Ending a call you placed

`POST /v1/calls/{id}/end`

**Shell**

```bash
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](https://voice.sphoro.com/docs/transfers).
