> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.talkif.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.talkif.ai/_mcp/server.

# Campaigns

> Call a list of contacts with one flow — how contacts are chosen, paced and retried, how call windows and contact timezones work, the campaign lifecycle, auto-pause, and what the analytics tell you.

A campaign is a list of contacts, a flow, a from-number, and rules about *how fast* and *when*. You start it and Talkif works through the list: placing calls up to your concurrency, only inside the hours you allow, retrying the ones that didn't connect, skipping anyone on the Do Not Call list, and stopping when the list is done or you tell it to. The result is per-contact outcomes and an analytics view of the whole run.

## What a campaign is made of

| Setting                      | What it decides                                                                        | Default              |
| ---------------------------- | -------------------------------------------------------------------------------------- | -------------------- |
| **Flow**                     | The published flow every call runs (latest version at the time each call is placed)    | —                    |
| **From number**              | Caller ID and routing provider — must be able to place calls                           | —                    |
| **Contacts**                 | Who gets called. Add individually, by filter (tags, company, search), or all           | —                    |
| **Concurrent calls**         | How many calls run at once, 1–50. Also bounded by your account's concurrent-call limit | 5                    |
| **Call windows**             | When calls may be placed: start–end times, optionally per weekday. None = any time     | none                 |
| **Respect contact timezone** | Evaluate windows in each contact's local time, not yours                               | on                   |
| **Start at / End at**        | Start automatically at a time; auto-pause at a time                                    | manual start; no end |

### Contact timezones and call windows

Windows are wall-clock times. With **Respect contact timezone** on (the default), a 09:00–17:00 window means 9 to 5 *where the contact is* — a list spanning New York and Berlin is called at sensible local hours automatically. A contact's timezone comes from their record, or is derived from their phone number when not set.

Separately from your windows, Talkif enforces **legal calling hours** by jurisdiction (for US numbers, 8 am – 9 pm local, per TCPA): a contact outside those hours isn't refused, they're held until the hours open. Your windows can be tighter than the law; never looser.

### Retries

A contact whose call ends `no_answer`, `busy`, `failed` or `canceled` is retried automatically, up to **3 attempts** per contact, with a delay between attempts so the same person isn't redialled back-to-back. A contact whose call was *answered* is done — a completed conversation is never redialled, whatever was said in it. A call that reaches voicemail counts as an attempt; see [Transfer and end calls](/build/transfer-and-end-calls#safety-net-3-voicemail-outbound-only) for leaving a message versus hanging up.

### Skipping

A contact is **skipped** — never called — when their number is on the [Do Not Call list](/calls/do-not-call), when they opted out, or when you skip them manually ([`POST …/contacts/{contactId}/skip`](/api-reference/campaigns/skip-campaign-contact)). Skipped contacts don't hold a campaign open.

## Lifecycle

```mermaid
stateDiagram-v2
  direction LR
  [*] --> draft
  draft --> pending: start
  pending --> running
  running --> draining: pause / cancel
  draining --> paused
  draining --> canceled
  running --> canceled: cancel (no active calls)
  running --> completed: list finished
  paused --> running: resume
  paused --> running: restart
  completed --> running: restart
  canceled --> running: restart
```

**`draft`** `editable`

Being configured. Add contacts, change settings.

---

**`pending`** `transient`

You pressed start; contacts are being queued.

---

**`running`** `active`

Placing calls within concurrency and windows.

---

**`draining`** `transient`

Pause or cancel requested; no new calls start, active calls finish.

---

**`paused`** `stopped`

Fully stopped. Add contacts, change settings, resume.

---

**`completed`** `terminal`

Every contact reached a final state.

---

**`canceled`** `terminal`

Stopped for good — unless restarted.

---

**Pause** always drains: calls in progress finish, nothing new starts. **Cancel** drains too if calls are active. **Restart** from paused, completed or canceled resets every contact that didn't complete back to pending and runs again — useful for a second pass at the no-answers.

### Auto-pause

A running campaign pauses itself, with a notification, when it can't safely continue:

| Reason               | What happened                                                          |
| -------------------- | ---------------------------------------------------------------------- |
| Insufficient balance | Your balance fell below the minimum to admit calls                     |
| System limit         | The account's concurrency or rate limit was hit for a sustained period |
| Provider error       | The from-number's provider is failing calls                            |
| End time             | `endAt` was reached                                                    |

Fix the cause and resume; contacts pick up where they were.

## Per-contact status

Each contact's status is derived from their latest call, so it's always consistent with call history:

**`pending`** `not yet called`

Waiting for a slot and a window.

---

**`queued`** `in progress`

In the call queue or being placed.

---

**`calling`** `in progress`

On a call now.

---

**`completed`** `final`

Answered and finished.

---

**`no_answer`** `retry or final`

Didn't answer or was busy; retried until attempts run out.

---

**`failed`** `retry or final`

The call errored; retried until attempts run out.

---

**`skipped`** `final`

Never called — DNC, opt-out or manual skip.

---

When a campaign ends with contacts never dialled (cancelled early, end time reached), they're counted as **not called** in the overview.

## Setting one up

#### Dashboard

#### Campaigns → New campaign

Name it; pick the flow and the from-number.

#### Add contacts

Individually, or by filter — a tag, a company, a search — to add hundreds at once. Contacts on the DNC list are added but will be skipped.

#### Set pacing and windows

Concurrency to match what your flow and your team can absorb (a flow that creates tickets at 50 concurrent calls creates tickets fast). Windows in the contacts' local time.

#### Start, or schedule the start

Start now, or set **Start at**. Watch the first few calls' transcripts before letting it run — a prompt problem shows up in call three, not call three hundred.

#### API

### Request

POST [https://api.talkif.ai/api/v1/campaigns](https://api.talkif.ai/api/v1/campaigns)

```curl
curl -X POST https://api.talkif.ai/api/v1/campaigns \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "flowId": "550e8400-e29b-41d4-a716-446655440000",
  "fromPhoneNumber": "+15551234567",
  "name": "January Outreach",
  "providerId": "550e8400-e29b-41d4-a716-446655440000"
}'
```

```typescript
import { TalkifClient } from "@talkif/sdk";

async function main() {
    const client = new TalkifClient({
        token: "YOUR_TOKEN_HERE",
    });
    await client.campaigns.createCampaign({
        flowId: "550e8400-e29b-41d4-a716-446655440000",
        fromPhoneNumber: "+15551234567",
        name: "January Outreach",
        providerId: "550e8400-e29b-41d4-a716-446655440000",
    });
}
main();

```

```python
import requests

url = "https://api.talkif.ai/api/v1/campaigns"

payload = {
    "flowId": "550e8400-e29b-41d4-a716-446655440000",
    "fromPhoneNumber": "+15551234567",
    "name": "January Outreach",
    "providerId": "550e8400-e29b-41d4-a716-446655440000"
}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.talkif.ai/api/v1/campaigns"

	payload := strings.NewReader("{\n  \"flowId\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"fromPhoneNumber\": \"+15551234567\",\n  \"name\": \"January Outreach\",\n  \"providerId\": \"550e8400-e29b-41d4-a716-446655440000\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.talkif.ai/api/v1/campaigns")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"flowId\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"fromPhoneNumber\": \"+15551234567\",\n  \"name\": \"January Outreach\",\n  \"providerId\": \"550e8400-e29b-41d4-a716-446655440000\"\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.talkif.ai/api/v1/campaigns")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"flowId\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"fromPhoneNumber\": \"+15551234567\",\n  \"name\": \"January Outreach\",\n  \"providerId\": \"550e8400-e29b-41d4-a716-446655440000\"\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.talkif.ai/api/v1/campaigns', [
  'body' => '{
  "flowId": "550e8400-e29b-41d4-a716-446655440000",
  "fromPhoneNumber": "+15551234567",
  "name": "January Outreach",
  "providerId": "550e8400-e29b-41d4-a716-446655440000"
}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.talkif.ai/api/v1/campaigns");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"flowId\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"fromPhoneNumber\": \"+15551234567\",\n  \"name\": \"January Outreach\",\n  \"providerId\": \"550e8400-e29b-41d4-a716-446655440000\"\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [
  "flowId": "550e8400-e29b-41d4-a716-446655440000",
  "fromPhoneNumber": "+15551234567",
  "name": "January Outreach",
  "providerId": "550e8400-e29b-41d4-a716-446655440000"
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.talkif.ai/api/v1/campaigns")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Request

POST [https://api.talkif.ai/api/v1/campaigns/\{campaignId}/contacts/bulk](https://api.talkif.ai/api/v1/campaigns/\{campaignId}/contacts/bulk)

```curl
curl -X POST https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{
  "filter": {}
}'
```

```typescript
import { TalkifClient } from "@talkif/sdk";

async function main() {
    const client = new TalkifClient({
        token: "YOUR_TOKEN_HERE",
    });
    await client.campaigns.bulkAddCampaignContacts({
        campaignId: "campaignId",
    });
}
main();

```

```python
import requests

url = "https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk"

payload = {}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk', [
  'body' => '{}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.talkif.ai/api/v1/campaigns/campaignId/contacts/bulk")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Request

POST [https://api.talkif.ai/api/v1/campaigns/\{campaignId}/start](https://api.talkif.ai/api/v1/campaigns/\{campaignId}/start)

```curl
curl -X POST https://api.talkif.ai/api/v1/campaigns/campaignId/start \
     -H "Authorization: Bearer <token>" \
     -H "Content-Type: application/json" \
     -d '{}'
```

```typescript
import { TalkifClient } from "@talkif/sdk";

async function main() {
    const client = new TalkifClient({
        token: "YOUR_TOKEN_HERE",
    });
    await client.campaigns.startCampaign({
        campaignId: "campaignId",
    });
}
main();

```

```python
import requests

url = "https://api.talkif.ai/api/v1/campaigns/campaignId/start"

payload = {}
headers = {
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.talkif.ai/api/v1/campaigns/campaignId/start"

	payload := strings.NewReader("{}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api.talkif.ai/api/v1/campaigns/campaignId/start")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.talkif.ai/api/v1/campaigns/campaignId/start")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.talkif.ai/api/v1/campaigns/campaignId/start', [
  'body' => '{}',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api.talkif.ai/api/v1/campaigns/campaignId/start");
var request = new RestRequest(Method.POST);
request.AddHeader("Authorization", "Bearer <token>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "Authorization": "Bearer <token>",
  "Content-Type": "application/json"
]
let parameters = [] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.talkif.ai/api/v1/campaigns/campaignId/start")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

Then [pause](/api-reference/campaigns/pause-campaign), [resume](/api-reference/campaigns/resume-campaign), [cancel](/api-reference/campaigns/cancel-campaign), [restart](/api-reference/campaigns/restart-campaign), and read progress with [`GET /campaigns/{campaignId}`](/api-reference/campaigns/get-campaign) or by subscribing to [real-time events](/integrate/real-time-events).

## Reading the analytics

[`GET /campaigns/{campaignId}/analytics`](/api-reference/analytics/get-campaign-analytics) (and the campaign's **Analytics** tab) gives the run a shape:

* **Outcomes** — completed / no answer / failed / skipped, as counts and percentages.
* **Attempt distribution** — how many contacts needed 1, 2 or 3 attempts. A heavy tail means your windows are wrong for the list.
* **Voicemail** — voicemail / human / undetermined for calls with a detection verdict. A high voicemail rate on a B2C list usually means daytime windows.
* **Failure causes** — grouped by structured cause, most common first, so one bad from-number or one provider outage is obvious.
* **Duration histogram** — an average hides the bimodal "hung up in 5 seconds" vs "had the conversation" split; the buckets don't.
* **Cost breakdown** and **performance** (latency, turns, function calls) across the run.

## Next

#### [Import contacts](/calls/import-contacts)

Get the list in from CSV, JSON or vCard.

#### [Do Not Call](/calls/do-not-call)

Who a campaign will never dial.