Run a Dialer Campaign from the API
Create a dialer campaign, load contacts, check the price, start it, and receive the results through your answer callback and webhooks.
Create a dialer campaign, load contacts, check the price, start it, and receive the results through your answer callback and webhooks.
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, 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.
dialer.write scope to create and run campaigns, and dialer.read to read them. Create a key.https endpoint for the answer callback. Private addresses and localhost are refused.progressive or predictive campaigns, a plan that includes them. Otherwise create and start answer 402 plan.required. Plain drip campaigns need no plan.Create a dialer campaign as a draft. Nothing is dialled until you start it. Money fields are in minor units, so 500000 is KES 5,000.
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"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.
Add contacts 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.
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 reasonOne bad row never fails the upload. Numbers on your suppression list are counted and never dialled.
Get the estimate first. It prices the worst case for the whole list. Then start the campaign. Starting spends money and cannot be undone, but you can pause at any time.
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.
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.
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: {} },
],
});
});To connect the person to someone, reply with a dial action instead of hangup. The 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.
Subscribe a webhook 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.
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 or export every attempt as CSV.