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

# campaigns

> have an agent call a list of people from your own number, and read every conversation back.

a campaign dials every person in an **audience** (a CSV you upload) with one
of your deployed agents, from one of your own numbers, a few calls at a time,
with retries and an optional start time. every call is billed and recorded
like any [outbound call](/outbound-calls.md), and comes back with its transcript
and recording.

the flow:

1. upload the list as an audience.
2. create the campaign: the agent, the number to call from, the pace.
3. follow it, and read each call's transcript and recording.

## 1. upload an audience

`POST /v1/campaign-audiences` takes a multipart form.

**file** `file` *required*

the CSV: `.csv`, `.tsv` or `.txt`, up to 10 MB and 10,000 rows, comma /
tab / semicolon delimited. the first row is the header.

**name** `string` *required*

the audience's name, up to 120 characters.

**phoneColumn** `string` *required*

the header of the column with the phone numbers (case-insensitive). rows
whose number can't be read are skipped and counted in `skippedCount`.

**defaultCountryCode** `string`

added to numbers written without one. default `+91`.

**contextColumns** `string`

a JSON **string** (a form can't nest): the columns the agent is told about
for each person, each with what it means, e.g.
`[{"column": "city", "description": "where they live"}]`. left out, every
column.

```bash curl theme={null}
curl -X POST https://silk-api.rumik.ai/v1/campaign-audiences \
  -H "Authorization: Bearer rk_live_•••••••••" \
  -F "file=@leads.csv" \
  -F "name=September leads" \
  -F "phoneColumn=mobile" \
  -F 'contextColumns=[{"column": "city", "description": "where they live"}]'
```

```json theme={null}
{
  "id": "01a0b054-6f82-7193-ce0f-1a2b3c4d5e6f",
  "name": "September leads",
  "columns": ["name", "city", "mobile"],
  "phoneColumn": "mobile",
  "contextColumns": [{ "column": "city", "description": "where they live" }],
  "contactCount": 2,
  "skippedCount": 1,
  "sourceFilename": "leads.csv",
  "createdAt": "2026-10-01T09:00:00Z",
  "updatedAt": "2026-10-01T09:00:00Z"
}
```

not sure which column holds the numbers? `POST /v1/campaign-audiences/preview`
with just the `file` returns its columns, a few rows and
`suggestedPhoneColumn` without saving anything.

## what the agent knows about each person

a list reaches the agent two ways, and only these two:

* **context columns.** the chosen columns are added to the agent's prompt at
  the start of every call, with their descriptions:

  ```text theme={null}
  Details of the person you are calling (from the campaign list):
  - city (where they live): Pune
  ```

* **variables.** a [variable](/variables-and-tools.md#variables) whose
  `campaignColumn` names a column takes that person's cell — in the prompt,
  the greeting, and as an input to before-call tools. write `{first_name}` in
  the greeting, and create the variable with
  `{"name": "first_name", "campaignColumn": "name"}`.

a `{name}` that merely matches a header is **not** filled from it, and no
other column reaches the agent. when you create a campaign, leave out of
`contextColumns` any column a variable in the prompt already reads, so the
agent doesn't get it twice.

## 2. create the campaign

`POST /v1/campaigns`

**name** `string` *required*

the campaign's name, up to 120 characters.

**audienceId** `string` *required*

the audience to call.

**agentId** `string` *required*

the agent — its UUID or `ua_…` handle. it must be **deployed** (`409
    agent_not_deployed`).

**phoneLineKind** `string` *required*

`rented` (a number [rented through rumik](/phone-numbers.md)) or `sip_trunk`
(one of your [SIP trunks](/sip-trunks.md)). campaigns always call from your
own number; `GET /v1/campaigns/phone-lines` lists the ones you can use.

**phoneLineId** `string` *required*

the rented number's or SIP trunk's id.

**contextColumns** `object[]`

the audience columns added to the prompt, `[{ "column", "description" }]`.
left out, the audience's own choice; `[]`, none.

**maxConcurrentCalls** `integer` (default: 1)

calls kept live at once, up to your account's limit (`maxConcurrentCalls`
on `GET /v1/campaigns/phone-lines`).

**retryEnabled** `boolean` (default: false)

redial people who don't pick up.

**retryDelayMinutes** `integer`

minutes before a redial: 15, 20, 30 or 60.

**maxRetries** `integer`

redials per person, 1–6.

**scheduledAt** `string`

ISO 8601 time to start, up to 90 days ahead. left out, it starts now.

```bash curl theme={null}
curl -X POST https://silk-api.rumik.ai/v1/campaigns \
  -H "Authorization: Bearer rk_live_•••••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Diwali offer",
    "audienceId": "01a0b054-6f82-7193-ce0f-1a2b3c4d5e6f",
    "agentId": "ua_fe3277d8",
    "phoneLineKind": "sip_trunk",
    "phoneLineId": "019f7c10-2b4e-7d5f-8a6b-7c8d9e0f1a2b",
    "maxConcurrentCalls": 2,
    "retryEnabled": true,
    "retryDelayMinutes": 15,
    "maxRetries": 2
  }'
```

returns `201` with the campaign: `status` is `running`, or `scheduled` when
`scheduledAt` is set.

## 3. follow it and read the calls

`GET /v1/campaigns/{id}` has live counts in `stats` (`pending`, `dialing`,
`inProgress`, `retryWaiting`, `completed`, `failed`, `skipped`). `notice` says
why it is waiting — every line busy, or your account at its call limit —
and clears on its own. `lastError` says why it paused, for example the
balance ran out.

`GET /v1/campaigns/{id}/contacts?status=failed` lists people by where they
stand.

`GET /v1/campaigns/{id}/calls` returns every dial, newest first, with the
person's row and the conversation:

```bash curl theme={null}
curl https://silk-api.rumik.ai/v1/campaigns/01a0c165-7093-72a4-df10-2b3c4d5e6f70/calls \
  -H "Authorization: Bearer rk_live_•••••••••"
```

```json theme={null}
{
  "items": [
    {
      "attemptId": "01a0e387-92b5-74c6-f132-4d5e6f708192",
      "contactId": "01a0d276-81a4-73b5-e021-3c4d5e6f7081",
      "phoneNumber": "+919876543210",
      "contactData": { "name": "Asha", "city": "Pune", "mobile": "9876543210" },
      "attemptNumber": 1,
      "status": "completed",
      "startedAt": "2026-10-01T09:07:00Z",
      "endedAt": "2026-10-01T09:08:41Z",
      "endReason": "customer_ended",
      "durationSeconds": 97,
      "errorMessage": null,
      "call": {
        "id": "01a05b4d-b1ad-7be0-8d29-04b2fd7e7dca",
        "status": "completed",
        "channel": "phone",
        "transcript": [
          { "role": "assistant", "content": "Hi Asha, this is acme…" },
          { "role": "user", "content": "Haan, boliye." }
        ],
        "recordingUrl": "https://storage.googleapis.com/…?X-Goog-Signature=…",
        "durationSeconds": 97
      }
    }
  ],
  "count": 23,
  "page": 1,
  "totalPages": 2
}
```

`call` is the same record as `GET /v1/calls/{callId}`. it is `null` until the
dial becomes a call, and for a dial the carrier refused (`errorMessage` says
why). `recordingUrl` is signed and short-lived: list again for a fresh one.
pages hold up to 50 calls (`?page=2&limit=50`).

## pause, resume, stop, delete

| call | does |
| - | - |
| `POST /v1/campaigns/{id}/pause` | stops placing new calls; calls in progress finish. running campaigns only. |
| `POST /v1/campaigns/{id}/resume` | carries on from where it stopped: a paused campaign as it stood, a stopped one with the people its stop skipped put back. people already called are never called again. |
| `POST /v1/campaigns/{id}/stop` | skips everyone not called yet and ends it (`status: cancelled`). calls in progress finish; `resume` puts the skipped people back. |
| `DELETE /v1/campaigns/{id}` | removes a scheduled or finished campaign. its calls stay in your call history. |

a transition the campaign's status doesn't allow answers `409 campaign_state`.

## how it dials

* **pace.** up to `maxConcurrentCalls` live at once, never more than your
  account's free call slots (your other calls hold slots too).
* **busy lines and limits are waits, not failures.** when every line is busy,
  your account is at its call limit, or your balance is held by calls in
  progress, the person waits for the next free slot and `notice` says why.
* **retries.** a call nobody answered is redialled after `retryDelayMinutes`
  while retries remain, else the person is `failed`. a call the carrier
  dropped because the line was full is redialled after a minute without using
  a retry.
* **billing.** every call is billed like a phone call. if the balance runs
  out, the campaign pauses with `lastError`; add funds and resume.

## errors

| status | code | when |
| - | - | - |
| 404 | `campaign_not_found` · `campaign_audience_not_found` · `agent_not_found` · `campaign_phone_line_not_found` · `sip_trunk_not_found` | not on this account. |
| 409 | `agent_not_deployed` | the agent has no live version. |
| 409 | `campaign_state` | the status doesn't allow the transition. |
| 409 | `campaign_audience_in_use` | deleting an audience a scheduled, running or paused campaign uses. |
| 422 | `invalid_request` | a field is missing or malformed, the line is not one of your numbers, or a column isn't in the audience. |
| 503 | `agent_phone_not_configured` | phone calls are unavailable on this deployment. |


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