> ## Documentation Index
> Fetch the complete documentation index at: https://docs.60db.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Outbound Campaigns

> Call a list of numbers with one agent. Set the calling windows, retries and concurrency, and watch the results

## Overview

A **campaign** calls a list of phone numbers with one agent. You give it an agent, a phone line, and a CSV file of recipients. The campaign places calls, up to its concurrency limit. Each call uses the agent's script. The values in the CSV fill the agent's `{{variables}}`, so each call can use the recipient's name or other details.

Each call goes through the same checks as any other call: AI credits, the Dialer balance on a 60db line, and the workspace call limit. A campaign pauses when one of these checks stops it. See [Statuses and pause reasons](#statuses-and-pause-reasons).

You can manage campaigns in the dashboard at `/app/agents/campaigns`, or with the API. The API reference starts at [List campaigns](https://docs.60db.ai/api-reference/agents/list-campaigns.md).

## Before you start

| You need | Why |
| - | - |
| An agent | The agent's script and voice are used for every call |
| A phone line that is ready | A line that is not ready is refused with `LINE_NOT_READY` |
| An active plan | Campaigns place calls and spend credits. Checking a file, creating a campaign and resuming one all need an active plan |
| A recipient CSV | Each row is one recipient. See [CSV format](#csv-format) |

Only call numbers whose owners agreed to be called. Add the others to the [do-not-call list](#do-not-call).

## CSV format

The first row is the header. The file needs one phone column.

```csv theme={null}
phone_number,name,city
+919876543210,Asha Rao,Pune
+919812345678,Ravi Kumar,Delhi
```

**Phone column.** The header must be one of `phone`, `phone_number`, `number` or `mobile`. Case, spaces and punctuation are ignored, so `Phone Number` also works. If a file has more than one of these, the first match in this list is used.

**Phone numbers.** Use an international number, with `+` or `00` in front, such as `+919876543210`. A number without `+` or `00` uses the phone line's default country code. A number that cannot be made international is skipped with the reason `invalid_number`.

**Other columns become variables.** Every other column becomes a variable. Its key is made from the header: lowercase letters, digits and `_`. For example, `customer name` becomes `customer_name`. If two headers give the same key, the first column is used. A value is kept up to 500 characters. A row whose variables are larger than 4 KB is skipped with the reason `variables_too_large`.

**File rules.**

| Rule | Limit |
| - | - |
| File size | 5 MB |
| Rows | 20,000 |
| Columns | 50 |
| Cell size | 2,000 characters |
| Delimiter | Comma, semicolon, tab or pipe. Detected from the first row |
| Quoting | Standard CSV quotes. Quoted fields can contain commas and line breaks |
| Duplicate headers | Refused |
| Duplicate numbers | Only the first row with a number is kept. Later rows are dropped |
| Blank rows | Skipped |

A file over a limit, or a file the parser cannot read, is refused with `CAMPAIGN_INVALID`. Check the file before you create the campaign. See [Preview](#preview).

## Variables

The agent's script and greeting use placeholders such as `{{customer_name}}`. A placeholder can also have a fallback, as in `{{customer_name|there}}`. A key uses lowercase letters, digits and `_`, up to 64 characters.

For each recipient and each agent variable, 60db picks the value like this:

1. The CSV column that the variable is mapped to, if the column has a value for this row. Without a mapping, the column with the same key is used.
2. Otherwise, the text in `variable_defaults` for that variable.
3. Otherwise, the variable is not set. The call then uses the fallback written in the placeholder, if there is one.

Use `variable_mapping` when the variable and the column have different names. The value in the mapping is a column key, that is, the lowercase form of the header. The mapped column must exist in the CSV.

```json theme={null}
{
  "variable_mapping": { "customer_name": "name" },
  "variable_defaults": { "plan": "standard" }
}
```

With this mapping, the agent's `customer_name` comes from the `name` column. The agent's `plan` comes from the default `standard` for every row, because the CSV has no `plan` column.

## Preview

Preview checks the file without storing anything. It shows:

* how many rows can be called, and how many cannot, with the reason for each
* how many rows are duplicates, and how many are on the do-not-call list
* for each agent variable, the CSV column that fills it by default and how many rows have a value

Send the agent's `agent_id` with the preview to get the variable check. See [Preview campaign recipients](https://docs.60db.ai/api-reference/agents/preview-campaign-recipients.md).

## Start now or later

Set `start` when you create the campaign:

* `"now"` starts the campaign on the next scheduler tick. It becomes `running`.
* An ISO 8601 time with an offset or `Z`, in the future, such as `2026-12-01T09:00:00+05:30`, makes the campaign `scheduled`. It starts at that instant.

A time without an offset is refused. This stops a server's local time zone from being used by mistake.

## Time zone and calling windows

The campaign `timezone` is an IANA name, such as `Asia/Kolkata`. It sets the calling windows and nothing else. `start` is always an exact instant.

A calling window is a day list and a time range in that time zone. The default is every day, from 09:00 to 20:00. You can set up to 20 windows. Days use ISO numbers: 1 is Monday and 7 is Sunday. A window cannot cross midnight. Its end can be `24:00`.

```json theme={null}
"calling_windows": [
  { "days": [1, 2, 3, 4, 5], "start": "09:00", "end": "13:00" },
  { "days": [1, 2, 3, 4, 5], "start": "15:00", "end": "20:00" }
]
```

Nothing dials outside a window. A retry that is due outside a window waits for the next window to open.

## Retries

| Setting | Default | Range | What it does |
| - | - | - | - |
| `max_attempts` | 3 | 1 to 30 | Total calls to each recipient, including the first call |
| `retry_delay_secs` | 1800 (30 minutes) | 0 to 604800 | Wait before the next attempt |
| `max_consecutive_failures` | none | 1 to 1000, or none | Pause the campaign after this many calls in a row that did not connect |

Each finished call sets the recipient's result:

* **Answered.** The call connected. The recipient is `completed`. The failure count goes back to zero.
* **No answer.** The carrier reported busy, no answer, a timeout or a decline, or the call ended without connecting. The recipient is retried, or is `no_answer` when no attempts are left.
* **Failed.** Any other result that did not connect. The recipient is retried, or is `failed` when no attempts are left.

A dial that could not start does not use an attempt. This happens when the workspace is at its call limit, or when a short service problem stops the dial. The recipient is tried again soon.

`max_consecutive_failures` counts calls in a row that did not connect. An answered call resets the count. When the count reaches the limit, the campaign pauses with the reason `consecutive_failures`. Resuming the campaign resets the count.

## Concurrency

Concurrency is the number of calls a campaign runs at the same time.

| Phone line | Concurrency |
| - | - |
| 60db Dialer number | Fixed at 3. A value you send is ignored |
| Your own line (`byo`) | 1 to 50. Default 3 |

The campaign also has to fit the workspace call limit. The limit that applies is the lower of the two. The campaign response shows `effective_concurrency` (the number that applies), and `workspace_concurrency` (the workspace limit, or `null` for no limit). A new call starts when a call in the campaign ends.

## Do-not-call

The do-not-call list belongs to the workspace. It is shared by all campaigns. A number on the list is:

* skipped when you upload the file, with the reason `dnc`
* skipped again when its call comes up, if you added it after the upload

Adding a number does not end a call that is already running. Manage the list with the API: [List](https://docs.60db.ai/api-reference/agents/list-do-not-call.md), [Add](https://docs.60db.ai/api-reference/agents/add-do-not-call.md) and [Remove](https://docs.60db.ai/api-reference/agents/remove-do-not-call.md).

## Statuses and pause reasons

**Campaign status**

| Status | Meaning |
| - | - |
| `scheduled` | Waiting for its start time |
| `running` | Dialing inside the calling windows |
| `paused` | No new calls. The `pause_reason` says why |
| `completed` | No recipient is `pending` or `calling` any more |
| `cancelled` | Stopped for good. Live calls finished |

`draft` is a valid filter value, but creating a campaign never returns it.

**Pause reasons**

| `pause_reason` | Set when |
| - | - |
| `user` | You paused the campaign |
| `consecutive_failures` | The failure limit was reached |
| `insufficient_credits` | The workspace does not have enough AI credits for the next call |
| `dialer_balance_empty` | The Dialer balance on a 60db line is used up |
| `plan_inactive` | The workspace plan is no longer active. Resume after the plan is active again |
| `line_not_ready` | The phone line is not ready to place calls |
| `line_unavailable` | The phone line was deleted |
| `agent_unavailable` | The agent was deleted |

**Recipient status**

| Status | Meaning |
| - | - |
| `pending` | Waiting for its next attempt |
| `calling` | A call is in progress or is about to start |
| `completed` | Answered |
| `no_answer` | Did not connect, and no attempts are left |
| `failed` | Failed to connect, and no attempts are left |
| `skipped` | Not called. `skip_reason` is `invalid_number`, `dnc` or `variables_too_large` |
| `cancelled` | The campaign was cancelled before this recipient was called |

## Controls

* **Pause** stops new calls. Live calls finish. Pausing works even when the plan is not active.
* **Resume** starts a paused campaign again. It needs an active plan. It resets the failure count.
* **Cancel** stops the campaign for good. Recipients still waiting become `cancelled`. Live calls finish.

A campaign that has finished cannot be resumed.

## Live progress

The campaign page in the dashboard shows the counts and the recipient table for the campaign. Each recipient shows its number, its variables, its status, its attempts, its last outcome and its next attempt.

The same data is available through the API:

* [Get campaign](https://docs.60db.ai/api-reference/agents/get-campaign.md) returns the `counts` and the concurrency.
* [List campaign recipients](https://docs.60db.ai/api-reference/agents/list-campaign-recipients.md) returns each recipient, with a filter by status. `last_call_id` links to the recipient's last call.
* [Get campaign summary](https://docs.60db.ai/api-reference/agents/get-campaign-summary.md) returns the number of running campaigns, waiting recipients and calls in progress for the workspace.
* Each call a campaign places has `metadata.campaign_id` and `metadata.recipient_id`. See [Get call](https://docs.60db.ai/api-reference/agents/get-call.md).

## Who can do what

| Action | Roles |
| - | - |
| See campaigns and recipients | Owner, admin, developer, member, viewer |
| Check a file with preview, or create a campaign | Owner, admin, developer, member. Needs an active plan |
| Resume a campaign | Owner, admin, developer, member. Needs an active plan |
| Pause, cancel, change the do-not-call list | Owner, admin, developer, member |

API keys need the `agents` scope.

## Example: create a campaign with the API

This example uses `jq` to build the JSON body from a file. Replace the placeholder ids.

```bash theme={null}
# 1. Check the file
jq -Rs --arg agent "AGENT_ID" '{csv_text: ., agent_id: $agent}' recipients.csv > preview.json
curl -X POST https://api.60db.ai/agents/campaigns/recipients/preview \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  --data @preview.json

# 2. Create the campaign
jq -Rs '{
  name: "Spring renewals",
  agent_id: "AGENT_ID",
  phone_line_id: "PHONE_LINE_ID",
  timezone: "Asia/Kolkata",
  start: "now",
  csv_text: .
}' recipients.csv > campaign.json
curl -X POST https://api.60db.ai/agents/campaigns \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: spring-renewals-2026-11-12" \
  --data @campaign.json
```

Use the same `Idempotency-Key` when you retry step 2. The retry returns the first campaign and does not create a second one. See [Create campaign](https://docs.60db.ai/api-reference/agents/create-campaign.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.