---
title: Run a Dialer Campaign from the API
description: >-
  Create a dialer campaign, load contacts, check the price, start it, and
  receive the results through your answer callback and webhooks.
summary: >-
  Five calls take a list of numbers to a running campaign. SautiKit dials at the
  rate you set, posts to your callback when a contact answers, and reports every
  result through webhooks.
date: 2026-10-20T00:00:00.000Z
type: guide
---


## Summary

A dialer campaign is one list of contacts, one caller ID, one callback URL and one set of rules. You create it as a draft, add contacts, check the estimate and start it. SautiKit places the calls at the rate you set, inside the hours you allow. When a contact answers, SautiKit posts to your callback URL and runs the voice actions you reply with. Results arrive as webhooks.

This guide shows the order of the calls and the two handlers you write. Every field, error and response is in the [API reference](/developers/api/createDialerCampaign), so it is not repeated here.

The same campaigns can also be set up in SautiDesk, with a recorded or typed message and no code. See [calling campaigns in SautiDesk](https://sautidesk.com/dialer).

## Prerequisites

- An API key with the `dialer.write` scope to create and run campaigns, and `dialer.read` to read them. [Create a key](https://app.sautikit.com/developers/api-keys).
- A number on your workspace to use as the caller ID.
- A public `https` endpoint for the answer callback. Private addresses and `localhost` are refused.
- A funded [wallet](/developers/concepts/wallet). Starting and resuming a campaign spend wallet money.
- For `progressive` or `predictive` campaigns, a plan that includes them. Otherwise create and start answer `402 plan.required`. Plain `drip` campaigns need no plan.

---

## Steps

### Step 1: Create the campaign

[Create a dialer campaign](/developers/api/createDialerCampaign) as a draft. Nothing is dialled until you start it. Money fields are in minor units, so `500000` is KES 5,000.

**JavaScript**

```javascript
const BASE = "https://api.sautikit.com";
const headers = {
  Authorization: `Bearer ${process.env.SAUTIKIT_API_KEY}`,
  "Content-Type": "application/json",
};

const res = await fetch(`${BASE}/v1/dialer/campaigns`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "October reminders",
    caller_id: "+254709000001",
    callback_url: "https://example.com/dialer/answer",
    timezone: "Africa/Nairobi",
    release_rate_per_sec: 1,
    max_concurrent: 20,
    budget_minor: 500000,
    hours: {
      timezone: "Africa/Nairobi",
      windows: [{ days: [1, 2, 3, 4, 5], start_minute: 540, end_minute: 1020 }],
    },
    retry: { max_attempts: 3, backoff_minutes: [15, 120, 1440], retry_on: ["no_answer", "busy", "failed"] },
  }),
});
const campaign = await res.json();
// Store campaign.callback_secret now. It is shown once.
console.log(campaign.id, campaign.status); // "draft"
```

**Python**

```python
import os, requests

BASE = "https://api.sautikit.com"
headers = {"Authorization": f"Bearer {os.environ['SAUTIKIT_API_KEY']}"}

res = requests.post(
    f"{BASE}/v1/dialer/campaigns",
    headers=headers,
    json={
        "name": "October reminders",
        "caller_id": "+254709000001",
        "callback_url": "https://example.com/dialer/answer",
        "timezone": "Africa/Nairobi",
        "release_rate_per_sec": 1,
        "max_concurrent": 20,
        "budget_minor": 500000,
        "hours": {
            "timezone": "Africa/Nairobi",
            "windows": [{"days": [1, 2, 3, 4, 5], "start_minute": 540, "end_minute": 1020}],
        },
        "retry": {"max_attempts": 3, "backoff_minutes": [15, 120, 1440], "retry_on": ["no_answer", "busy", "failed"]},
    },
)
campaign = res.json()
# Store campaign["callback_secret"] now. It is shown once.
print(campaign["id"], campaign["status"])  # "draft"
```

The response carries `callback_secret`, the key that signs your answer callback. Keep it with your other secrets. To pick the calling type, set `mode` to `drip`, `progressive` or `predictive`.

### Step 2: Add contacts

[Add contacts](/developers/api/appendDialerContacts) as JSON or CSV. Anything you put in `fields` is sent back to your callback when that contact answers, so use it for names and amounts. Send an `Idempotency-Key` so a retried upload is never added twice.

**JavaScript**

```javascript
const added = await fetch(`${BASE}/v1/dialer/campaigns/${campaign.id}/contacts`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "october-reminders-batch-1" },
  body: JSON.stringify({
    contacts: [
      { phone: "+254700000002", external_ref: "crm-1042", fields: { name: "Amina", balance: 1200 } },
      { phone: "+254700000003", external_ref: "crm-1043", fields: { name: "Joseph", balance: 800 } },
    ],
  }),
}).then((r) => r.json());

console.log(added.accepted, added.rejected); // rows that could not be added come back with a reason
```

**Python**

```python
added = requests.post(
    f"{BASE}/v1/dialer/campaigns/{campaign['id']}/contacts",
    headers={**headers, "Idempotency-Key": "october-reminders-batch-1"},
    json={
        "contacts": [
            {"phone": "+254700000002", "external_ref": "crm-1042", "fields": {"name": "Amina", "balance": 1200}},
            {"phone": "+254700000003", "external_ref": "crm-1043", "fields": {"name": "Joseph", "balance": 800}},
        ]
    },
).json()

print(added["accepted"], added["rejected"])  # rows that could not be added come back with a reason
```

One bad row never fails the upload. Numbers on your suppression list are counted and never dialled.

### Step 3: Check the price, then start

[Get the estimate](/developers/api/getDialerCampaignEstimate) first. It prices the worst case for the whole list. Then [start the campaign](/developers/api/startDialerCampaign). Starting spends money and cannot be undone, but you can [pause](/developers/api/pauseDialerCampaign) at any time.

```bash
curl -s "https://api.sautikit.com/v1/dialer/campaigns/$ID/estimate" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"

curl -s -X POST "https://api.sautikit.com/v1/dialer/campaigns/$ID/start" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY"
```

If you turn on answering-machine detection with the `amd` setting, each detection is a flat KES 0.50 on top of the call itself.

### Step 4: Answer the callback

When a contact answers, SautiKit POSTs JSON to your callback URL and waits up to 15 seconds for the voice actions to run. The person is already listening, so reply fast. Verify the signature with the campaign's `callback_secret`, the same way as a [webhook](/developers/guides/verify-webhook-signatures).

**JavaScript**

```javascript
app.post("/dialer/answer", express.raw({ type: "application/json" }), (req, res) => {
  // Verify X-Sautikit-Signature against req.body (the raw bytes) first.
  const answer = JSON.parse(req.body);
  const name = answer.fields?.name ?? "there";

  res.json({
    actions: [
      { say: { text: `Hello ${name}, this is a reminder that your payment is due tomorrow.` } },
      { hangup: {} },
    ],
  });
});
```

**Python**

```python
@app.post("/dialer/answer")
def answer():
    # Verify X-Sautikit-Signature against request.get_data() first.
    answer = request.get_json()
    name = (answer.get("fields") or {}).get("name", "there")

    return jsonify({
        "actions": [
            {"say": {"text": f"Hello {name}, this is a reminder that your payment is due tomorrow."}},
            {"hangup": {}},
        ]
    })
```

To connect the person to someone, reply with a `dial` action instead of `hangup`. The [voice actions](/developers/concepts/voice-actions) page lists every verb. If your endpoint is slow, returns a 5xx or sends invalid actions, the contact hears a short apology and the attempt is recorded as `failed`, which your retry policy can try again.

### Step 5: Receive the results

Subscribe a [webhook](/developers/api/createWebhook) to the campaign events. The call events carry the campaign, contact and attempt, so you can match each result to your own record through `external_ref`.

```bash
curl -s -X POST "https://api.sautikit.com/v1/webhooks" \
  -H "Authorization: Bearer $SAUTIKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/sautikit/events",
    "events": ["dialer.call.completed", "dialer.campaign.completed", "dialer.budget.warning"]
  }'
```

`dialer.call.completed` says how each call ended, with `result`, `contact_status`, `duration_seconds` and the call cost. `dialer.campaign.completed` fires when every contact has reached a final state. `dialer.budget.warning` fires once when spend crosses `warn_pct`, and the campaign stops releasing calls when the budget is reached.

When it is over, read the [campaign report](/developers/api/getDialerCampaignReport) or [export every attempt as CSV](/developers/api/exportDialerAttemptsCsv).

---

## Next steps

- [Verify webhook signatures](/developers/guides/verify-webhook-signatures): the same check applies to the answer callback.
- [Voice broadcast campaigns](/use-cases/voice-broadcast-campaigns): campaigns where a published AI agent talks to each contact.
- [Webhooks](/developers/concepts/webhooks): delivery, retries and the event list.
