# Calls and campaigns

> Placing outbound calls, attempts and retries, calling windows, statuses, outcomes and campaigns.


# Calls and campaigns

## Place a call

```http
POST /v1/calls
Authorization: Bearer alt_live_…
Idempotency-Key: invoice-42

{
  "agent": "7d1c…",
  "to": "+393331234567",
  "inputs": { "name": "Luca Galli", "amount": 250, "month": "January" },
  "reference": "invoice-42",
  "scheduled_at": "2026-10-01T09:30:00+02:00",
  "max_attempts": 2,
  "callback_url": "https://your-app.example/allerto/outcomes"
}
```

| Field          | Required                     | Meaning                                                               |
| -------------- | ---------------------------- | --------------------------------------------------------------------- |
| `agent`        | yes                          | A published outbound agent                                            |
| `to`           | yes                          | E.164 number                                                          |
| `inputs`       | when the agent requires them | Validated against the agent's `inputs`                                |
| `reference`    | no                           | Your id; returned in every event                                      |
| `scheduled_at` | no                           | Earliest time for the first attempt                                   |
| `max_attempts` | no                           | Overrides `calling.maxAttempts` (1–5)                                 |
| `callback_url` | no                           | HTTPS URL receiving this call's events instead of the agent's webhook |

The response is `202` with the [call object](#the-call-object). Repeating the request with the same `Idempotency-Key` returns `200` and the same call: it is never dialled twice.

## What happens next

1. The queue waits for `scheduled_at` and for the agent's calling window (Italian time).
2. It checks the do-not-call list, the daily cap (3 attempts per number per day across the account) and the account's concurrent calls (3 by default).
3. The phone rings from the agent's caller ID. Answering machines are detected and hung up before the model speaks (`voicemail`, never retried).
4. No answer or busy: another attempt after 15 minutes, 1 hour, 3 hours, up to `max_attempts`. You receive `call.attempt_failed` for each.
5. After a conversation the transcript is turned into the agent's `result`, the outcome is classified and `call.completed` is sent.
6. `do_not_call` adds the number to your list. `callback_requested` with a time creates a new call at that time (inside the window) when `calling.autoCallback` is on; it links back with `retry_of`.

Cancel a call that has not started with `POST /v1/calls/{id}/cancel`.

## The call object

```json
{
  "id": "…",
  "object": "call",
  "direction": "outbound",
  "livemode": true,
  "agent": { "id": "…", "version": 3, "name": "Payment reminder" },
  "client": { "id": "…", "external_id": "default" },
  "campaign_id": null,
  "reference": "invoice-42",
  "to": "+393331234567",
  "from": "+390280781047",
  "inputs": { "name": "Luca Galli", "amount": 250, "month": "January" },
  "status": "completed",
  "outcome": "completed",
  "attempts": 1,
  "max_attempts": 3,
  "next_attempt_at": null,
  "answered_at": "…",
  "ended_at": "…",
  "duration_seconds": 94,
  "callback_at": null,
  "transfer": null,
  "tool_calls": [],
  "ai_disclosed": true,
  "recording": null,
  "anonymized_at": null,
  "result": {
    "data": { "answer": "will_pay", "pay_by": "2026-10-05" },
    "issues": [],
    "summary": "…",
    "sentiment": {
      "label": "negative",
      "score": -1,
      "start": -2,
      "end": 0,
      "trend": "improving",
      "emotions": [
        {
          "type": "frustration",
          "intensity": "high",
          "evidence": "it's the third time I've called"
        }
      ]
    }
  },
  "retry_of": null,
  "error": null,
  "created_at": "…"
}
```

Inbound calls have the same shape with `direction: "inbound"`, `from` set to the caller and no inputs.

**Status**: `queued` → `dialing` → `in_progress` → `processing` → `completed`; or `failed`, `cancelled`; `needs_review` while an operator checks the data (inbound agents with missing data).

**Outcome**:

| Outcome                                 | Answered | Retried                      | Meaning                                                    |
| --------------------------------------- | -------- | ---------------------------- | ---------------------------------------------------------- |
| `completed`                             | yes      | no                           | The conversation reached its purpose (whatever the answer) |
| `callback_requested`                    | yes      | automatic, see `callback_at` | Asked to be called later                                   |
| `wrong_person`                          | yes      | no                           | Someone else answered                                      |
| `refused`                               | yes      | no                           | Did not want to talk                                       |
| `do_not_call`                           | yes      | never                        | Asked not to be called again                               |
| `hung_up`                               | yes      | no                           | Ended before saying anything useful                        |
| `transferred`                           | yes      | no                           | Handed to the operator of the agent's `transfer` settings  |
| `no_answer`, `busy`                     | no       | yes                          |                                                            |
| `voicemail`                             | no       | no                           | Answering machine                                          |
| `suppressed`                            | no       | no                           | Number on the do-not-call list                             |
| `invalid_number`, `failed`, `cancelled` | no       | no                           |                                                            |

## Transfer

A call handed to an operator has `outcome: "transferred"` and:

```json
"transfer": {
  "to": "+390212345678",
  "status": "answered",
  "requested_at": "…",
  "ended_at": "…",
  "duration_seconds": 184
}
```

`status` is `requested` or `dialing` while the operator's phone rings, then `answered`, `no_answer`, `busy` or `failed`; `duration_seconds` is the time spent with the operator. `call.completed` is sent as soon as the data collected before the transfer is ready, usually while the transfer is still going on: read the call for its final status. The conversation with the operator is not transcribed.

## Sentiment

Every call where the person spoke comes with `result.sentiment`, for every agent and without configuration, like the summary.

- `score` goes from `-2` (very negative) to `2` (very positive); `0` is a calm, factual call, the usual case. `label` is `negative`, `neutral` or `positive`.
- `start` and `end` use the same scale for the first and last part of the call; `trend` says whether it was `improving`, `stable` or `worsening`.
- `emotions` lists up to three emotions the person expressed (`frustration`, `anger`, `anxiety`, `confusion`, `satisfaction`, `gratitude`), each with an `intensity` (`low`, `medium`, `high`) and the person's own words as `evidence`. An emotion is kept only if those words are really in the transcript.

It is read from what the person said, never from their voice: it is text analysis, not emotion recognition from biometric data. `sentiment` is `null` when the person did not speak or the call could not be analysed; test-mode calls carry a neutral sentiment.

## Reading calls

```http
GET /v1/calls?direction=outbound&outcome=callback_requested&since=2026-09-01T00:00:00Z&limit=50
GET /v1/calls/{id}
GET /v1/calls/{id}/transcript
GET /v1/calls/{id}/recording
```

`ai_disclosed` says whether the agent stated it is an AI in its first words (`null` when it never spoke). `recording` is `{ "url": "/v1/calls/{id}/recording" }` when the agent records calls: a mono 8 kHz WAV. After 24 months personal data is removed and `anonymized_at` is set; see [Privacy](privacy.md).

Lists are newest first; pass `next_cursor` as `cursor` for the next page. Filters: `direction`, `agent`, `campaign`, `status`, `outcome`, `reference`, `since`.

## Campaigns

```http
POST /v1/campaigns
{
  "agent": "7d1c…",
  "name": "January instalments",
  "contacts": [
    { "to": "+393331234567", "inputs": { "name": "Luca Galli", "amount": 250, "month": "January" }, "reference": "A-1" },
    { "to": "+393339876543", "inputs": { "name": "Anna Verdi", "amount": 90, "month": "January" }, "reference": "A-2" }
  ],
  "max_concurrent": 3
}
```

Every contact is validated before anything is queued: if one is invalid you get `400 invalid_contacts` with the index of each problem, and nothing is queued. Duplicates and numbers on the do-not-call list are skipped and reported in `skipped`. Up to 5,000 contacts per request.

```sh
allerto campaigns create --agent <id> --csv contacts.csv --name "January instalments"
```

The CSV needs a `to` column (also `phone`, `telefono`, `numero`); `reference` is optional; every other column is an input. Commas or semicolons both work, and Italian numbers like `333 1234567` become `+393331234567`.

`GET /v1/campaigns/{id}` returns counts `by_status` and `by_outcome`. `pause`, `resume` and `cancel` are `POST /v1/campaigns/{id}/<action>`; cancelling cancels the calls still queued.

## Do-not-call list

```http
GET    /v1/suppressions
POST   /v1/suppressions        {"number": "+393331234567", "reason": "Asked by email"}
DELETE /v1/suppressions/%2B393331234567
```

The list is checked when a call is queued and again right before dialling. Adding a number fails its queued calls with `suppressed`.
