Campaigns API
Build an outbound calling campaign over the API: create it, load the list, set the calling hours and dial mode, start it, watch it run, and read the per-contact result. Includes the account-wide do-not-call list.
Overview
A campaign is a list of people, a voice agent, a number to call from, and the rules for when and how fast to dial. You build it as a draft, load the list, then start it — and a background dialer works through the list, respecting your calling hours, your retry limit and the account's do-not-call list.
Base path: https://api.callmissed.com/api/v1/voice-campaigns
- 1
Create
makes a draft. Nobody is called yet
- 2
Load the list
or a CSV upload
- 3
Start
with — the dialer takes it from here
Authentication. Every endpoint accepts a JWT (Authorization: Bearer <jwt>) or an API key (Authorization: Bearer cm_<key>). API-key callers need campaigns:read for the list, get, contacts, live and do-not-call reads, and campaigns:write for everything that changes something — create, update, delete, add contacts, add a suppression, and every status change.
Starting a campaign places real calls to real people and draws down your credit balance. campaigns:write is the scope that allows it; give it only to a key that is meant to run campaigns.
Availability. Campaigns are enabled per account. Where the feature is off, every route below returns
404.
Campaign lifecycle
A campaign is always in exactly one status, and only certain moves are allowed. A move that is not allowed returns 409 naming both ends.
| From | May move to |
|---|---|
draft | scheduled, cancelled |
scheduled | running, paused, draft, cancelled |
running | paused, completed, cancelled |
paused | running, scheduled, cancelled |
insufficient_balance | running, paused, cancelled |
completed | — terminal |
cancelled | — terminal |
insufficient_balance is set for you when the account runs out of credits mid-campaign; you cannot post it. Top up, then move back to running.
When the dialer stops a running campaign on its own (no number to call from, the agent or number was removed, or a TRAI readiness check failed), it moves the campaign to paused and the campaign's status_reason says why. status_reason is only set while the campaign is paused or insufficient_balance, and is cleared when you move it back to scheduled or running. A campaign you paused yourself has status_reason: null.
A brand-new campaign is a draft, and draft cannot jump straight to running — go draft → scheduled → running. Contacts can only be added while the campaign is draft, scheduled or paused.
Create a campaign
POST / · scope campaigns:write · 201 Created
| Field | Type | Required | Notes |
|---|---|---|---|
name | string (1–255) | Yes | Your own label for the campaign |
bot_id | uuid | No | The voice agent that answers for you on each call |
from_number_id | uuid | No | Which of your numbers to call from |
caller_id_pool_id | uuid | No | Dial from a caller ID pool instead. While the pool is enabled it replaces from_number_id. Send null on a patch to detach |
scheduled_at | timestamp | No | When the campaign should begin |
timezone | string | No | IANA zone the calling hours are read in. Default Asia/Kolkata |
quiet_hours_start | integer 0–23 | No | Earliest local hour a call may be placed. Default 9 |
quiet_hours_end | integer 1–24 | No | Latest local hour. Must be after the start. Default 21 |
max_attempts | integer 1–5 | No | Retries per contact. Default 3 |
dial_mode | preview | power | parallel | No | Default power |
pace_seconds | integer 5–3600 | No | Seconds between calls in power mode. Default 30 |
parallel_lines | integer 1–5 | No | Calls at once in parallel mode. Default 1 |
amd_enabled | boolean | No | Detect an answering machine. Off by default |
amd_mode | classic | full_greeting | off | No | How a machine is detected while amd_enabled is on. Unset uses the agent's own amd_mode, else classic. See Answering-machine detection |
voicemail_template_id | uuid | No | Voicemail to leave on a machine. Omit to detect and hang up |
dlt_principal_entity_id | string (≤32, letters/digits) | No | Your India DLT principal entity ID. See TRAI readiness |
dlt_header | string (≤32) | No | The DLT header / caller ID these calls go out under |
dlt_template_id | string (≤64) | No | DLT content template ID, if you registered one |
dlt_consent_ref | string (≤128) | No | Your reference to the recorded consent these calls rely on |
Contacts with an Indian (+91) number are additionally only dialled between 10:00 and 21:00 IST, whatever the campaign's quiet hours say (TRAI TCCCPR 2018, default customer preference). Contacts outside that window stay pending and are picked up when it opens.
A campaign that carries only transactional calls (TRAI's definition: a call answering a transaction the customer started, within 30 minutes) can skip that window with "trai_exempt_transactional": true on create or update. Turning it on needs an owner or admin signed in to the dashboard (an API key gets 403), a from_number_id from the 1600 or 1601 series that is declared for automated calls on the Numbers page, and no caller ID pool, or it returns 422. The window applies again as soon as the campaign calls from any other number. Your campaign's own quiet hours still apply. Sending false turns it off, and any campaigns:write caller can do that.
A bot_id, from_number_id or voicemail_template_id your account does not own returns 404. A calling window that is empty or wraps midnight returns 422, and so does parallel_lines above 1 in the same request as a non-parallel dial_mode.
curl -X POST https://api.callmissed.com/api/v1/voice-campaigns \
-H "Authorization: Bearer cm_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 renewal outreach",
"bot_id": "0f2c...",
"from_number_id": "9a71...",
"timezone": "Asia/Kolkata",
"quiet_hours_start": 10,
"quiet_hours_end": 19,
"dial_mode": "power",
"pace_seconds": 45
}'{
"id": "6d1e...",
"tenant_id": "1b0a...",
"name": "Q3 renewal outreach",
"status": "draft",
"total_contacts": 0,
"completed_contacts": 0,
"failed_contacts": 0,
"timezone": "Asia/Kolkata",
"quiet_hours_start": 10,
"quiet_hours_end": 19,
"max_attempts": 3,
"dial_mode": "power",
"pace_seconds": 45,
"parallel_lines": 1,
"amd_enabled": false,
"amd_mode": null,
"created_at": "2026-09-20T09:00:00Z",
"updated_at": "2026-09-20T09:00:00Z"
}Dial modes
| Mode | What it does | Use it when |
|---|---|---|
preview | Nothing is dialled until a person approves each contact, one at a time | A human should see who is next before the phone rings |
power | One call at a time, pace_seconds apart | The default. Steady, predictable pacing |
parallel | Up to parallel_lines calls at once; the first answer wins | Long lists where most calls go unanswered |
parallel_lines above 1 requires dial_mode: "parallel". Switching a parallel campaign to another mode silently resets it to 1 line, so a power campaign never carries stale concurrency.
Answering-machine detection
With amd_enabled on, each answered call is checked for a voicemail greeting before your agent carries on. A machine gets your voicemail drop (voicemail_template_id) and the call ends; with no drop set, the call simply ends. Outbound calls placed outside a campaign use the same two keys, plus amd_mode, on the agent's config.
amd_mode | How it decides | When the drop plays |
|---|---|---|
classic (default) | The callee's first words are matched against known voicemail and network greetings in English, Hindi and Hinglish | As soon as a greeting phrase is recognised |
full_greeting | Listens to the whole greeting and classifies it with your agent's language model. A short spoken "hello" counts as a person straight away | After the greeting ends and the record beep has sounded |
off | No detection, even with amd_enabled on | Never |
In full_greeting mode your agent waits for the callee to speak first, so a person who picks up and stays silent hears your agent after a few seconds. A greeting saying no message can be left (a full or unset mailbox, a phone that is switched off or out of coverage) ends the call without a drop. A phone menu or a call screener asking who is calling is treated as a person, so your agent answers it. A spoken notice that the call is recorded never counts as a machine on its own, in either mode. Agents that run a speech-to-speech model or your own language model use classic detection.
The full_greeting classification runs on your agent's language model and is billed as that call's model usage, at the agent's model rate: typically one or two short requests per call. It adds no speech-to-text cost, because it reads the call's own transcript.
Every verdict is sent as the call.amd_detected webhook event. Its payload carries amd_result (human, machine or unknown), voicemail_dropped, amd_detector (classic or full_greeting) and, from full_greeting, amd_category (human, machine-vm, machine-unavailable, machine-ivr or uncertain).
List and read
GET / · scope campaigns:read · limit 1–100 (default 50), offset 0–100000
GET /{campaign_id} · scope campaigns:read
A campaign that is not yours returns 404, never another account's row.
Update
PATCH /{campaign_id} · scope campaigns:write
Accepts name, scheduled_at, from_number_id and every rail, answering-machine (amd_enabled, amd_mode, voicemail_template_id) and DLT field from the create table. bot_id cannot be changed after creation. Anything you leave out is kept; an empty string clears a DLT field. Status is not settable here — use the transition endpoint below, so the allowed-move table always applies.
Change status
POST /{campaign_id}/status · scope campaigns:write
curl -X POST https://api.callmissed.com/api/v1/voice-campaigns/6d1e.../status \
-H "Authorization: Bearer cm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"status": "running"}'Returns the updated campaign, or 409 if the move is not allowed. Posting the status it is already in is a no-op that returns 200.
Moving to scheduled or running runs the TRAI readiness check. If your account enforces it and the campaign is not ready, the move returns 409 naming what is missing.
Moving to running fires the campaign.started webhook; reaching completed fires campaign.completed. Pausing, cancelling and scheduling fire nothing — they are operator moves, not lifecycle beats a subscriber acts on.
Add people to the list
POST /{campaign_id}/contacts · scope campaigns:write · up to 1000 per request
| Field | Type | Required |
|---|---|---|
contacts[].phone | string (1–64) | Yes |
contacts[].name | string (0–255) | No |
curl -X POST https://api.callmissed.com/api/v1/voice-campaigns/6d1e.../contacts \
-H "Authorization: Bearer cm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"contacts": [
{"phone": "+919000000001", "name": "Ada"},
{"phone": "+919000000002"}
]}'{ "inserted": 2, "skipped_invalid": 0, "skipped_duplicate": 0, "total_now": 2, "dnc_matches": 0 }A row with no usable digits is counted in skipped_invalid rather than failing the whole batch. Numbers are deduplicated on their last 10 digits, against the contacts already on the campaign and within the request, so +919000000001, 919000000001 and 9000000001 are one person; repeats are counted in skipped_duplicate and are not called twice. dnc_matches counts the added numbers that are on your do-not-call list: they stay on the campaign but are never dialled. Adding to a campaign that is running, completed or cancelled returns 409.
On a campaign whose timezone is Asia/Kolkata (the default), an Indian mobile written without a leading + is saved in international format: 9000000001, 09000000001 and 919000000001 all become +919000000001. A number that starts with + is saved as you sent it. In any other timezone, send numbers with their + country code.
Upload a CSV
POST /{campaign_id}/contacts/import · scope campaigns:write · multipart/form-data
Each row is upserted into your CRM contacts and attached to the campaign, deduplicated on the last 10 digits against the numbers already on it and earlier in the file — so re-uploading the same file never queues a second call to the same person. Phone numbers are saved the same way as in Add people to the list.
| Form field | Type | Notes |
|---|---|---|
file | file | CSV, first row is the header. UTF-8 or Windows-1252; comma, semicolon or tab separated |
column_map | string | JSON object mapping your headers to our fields, e.g. {"Mobile No.":"phone"} |
dry_run | boolean | Default true — previews both effects and writes nothing |
opted_in | boolean | Asserts messaging consent for new contacts with no opt-in column of their own |
curl -X POST https://api.callmissed.com/api/v1/voice-campaigns/6d1e.../contacts/import \
-H "Authorization: Bearer cm_your_api_key" \
-F '[email protected]' \
-F 'column_map={"Mobile No.":"phone","Full Name":"name"}' \
-F 'dry_run=false'The response is the CRM import report plus attached, skipped_invalid, skipped_duplicate, total_now and dnc_matches (attached numbers on your do-not-call list, which are never dialled). Rows the CRM import rejected are never attached, so a row the report told you was skipped is not dialled.
Read the results
GET /{campaign_id}/contacts · scope campaigns:read · limit 1–500 (default 100), offset 0–1000000, optional status (pending, approved, calling, done or failed) to list only contacts in that state
Each row carries what happened to that person:
| Field | Notes |
|---|---|
status | pending, approved, calling, done or failed |
attempts | How many times it has been tried |
next_attempt_at | When the next retry is due, if any |
last_error | Why the last attempt did not complete |
call_id | The call this contact produced, for the Telephony API |
The campaign itself carries the totals: total_contacts, completed_contacts, failed_contacts.
Download the results as CSV
GET /{campaign_id}/contacts/export · scope campaigns:read · optional status filter as above
Streams one CSV row per contact: name, phone, state, attempts, last_error, next_attempt_at, call_id, call_status, call_duration_seconds and added_at. Cells a spreadsheet would read as a formula (including a leading + on a phone number) are prefixed with '. From the console this is limited to owners and admins.
curl -o results.csv https://api.callmissed.com/api/v1/voice-campaigns/6d1e.../contacts/export \
-H "Authorization: Bearer cm_your_api_key"Watch it run
GET /{campaign_id}/live · scope campaigns:read
Returns the calls this campaign has on the wire right now — each with the contact, the status, and in parallel mode which leg won the race — plus, in preview mode, the contacts waiting for a go-ahead and how many are queued behind them.
{
"campaign_id": "6d1e...",
"status": "running",
"dial_mode": "parallel",
"pace_seconds": 30,
"parallel_lines": 2,
"active_calls": [
{
"call_id": "b41f...",
"contact_id": "77aa...",
"phone": "+919000000001",
"name": "Ada",
"status": "in_progress",
"burst_id": "3f2d...",
"is_winner": true,
"started_at": "2026-09-20T10:14:02Z"
}
],
"awaiting_approval": [],
"awaiting_approval_total": 0
}Approve one contact in preview mode
POST /{campaign_id}/contacts/{contact_id}/approve · scope campaigns:write
A preview campaign dials only approved contacts, so this is what makes the phone ring. 409 on a campaign that is not in preview mode, or on a contact that is no longer waiting; 404 on a contact that is not on this campaign.
TRAI readiness for AI calls (India)
TRAI's Third Amendment to the TCCCPR (announced 18 September 2026, press release 119/2026) treats calls placed by an application or automated platform, including AI and artificial-voice calls, as A2P calls. Every business making them must declare that use, and the caller IDs it uses, to its telecom provider beforehand. An undeclared A2P call is treated as unsolicited commercial communication. Every campaign call is an A2P call.
These rules cover Indian caller IDs. A campaign calling from a number outside India has no Indian telecom provider to declare to, so the checks below do not apply to it, unless its caller ID pool also holds Indian numbers, in which case the DLT fields are still required.
You make the declaration with your telecom provider; CallMissed cannot file it for you. What CallMissed does:
- Records, per number, that you have declared it (
PUT /numbers/{number_id}/a2p-declaration). - Stores the DLT fields above on each campaign.
- Checks both before a campaign starts, and again before each dial.
A campaign is ready when all of these are true:
- The number it calls from is declared.
dlt_principal_entity_idanddlt_headerare set.- Either
dlt_template_idordlt_consent_refis set.
Enforcement is off by default. With it off, a campaign that is not ready still starts and the warning is written to your audit log. With it on, a campaign that is not ready cannot be scheduled or started. A running campaign that stops being ready, for example because its number's declaration is withdrawn, is paused before its next call. An account owner or admin switches enforcement on or off in the dashboard, under Campaigns. API keys cannot change it.
GET /{campaign_id}/trai-readiness · scope campaigns:read
curl https://api.callmissed.com/api/v1/voice-campaigns/6d1e.../trai-readiness \
-H "Authorization: Bearer cm_your_api_key"{
"enforced": true,
"ready": false,
"issues": [
{ "code": "number_not_declared", "message": "The calling number is not declared for automated/AI calls. ..." }
]
}code | Meaning |
|---|---|
no_from_number | No active number to call from |
number_not_declared | The calling number has no A2P declaration recorded |
dlt_principal_entity_missing | dlt_principal_entity_id is empty |
dlt_header_missing | dlt_header is empty |
dlt_consent_or_template_missing | Neither dlt_template_id nor dlt_consent_ref is set |
GET /trai-settings · scope campaigns:read returns {"enforce": false, "updated_at": null}.
Delete a campaign
DELETE /{campaign_id} · scope campaigns:write · 204
Deletes the campaign and its contact list. Calls it already placed are unaffected and stay in the Telephony API.
Do-not-call list
The do-not-call list is account-wide: a suppressed number is not contacted from anywhere — a campaign, the Telephony API, the WhatsApp API, an agent tool. Numbers are matched on their trailing digits, so the same person is suppressed whichever way the number was written.
Each entry has a channel that says what it blocks:
channel | Blocks |
|---|---|
all (default) | Calls and every WhatsApp message |
voice | Calls only |
whatsapp | Every WhatsApp message, not calls. Added automatically when a customer sends STOP |
whatsapp_marketing | WhatsApp MARKETING templates only. Added automatically when WhatsApp reports the person stopped marketing messages |
One number can have an entry for more than one channel. See Opt-outs for how WhatsApp sends treat each one.
GET /dnc · scope campaigns:read · limit 1–500 (default 100), offset 0–1000000, channel (optional, repeatable: ?channel=whatsapp&channel=whatsapp_marketing returns only those channels' entries)
Each entry comes back with id, phone, reason, source (manual, api, opt_out, wa_keyword or meta_131050), channel and created_at.
POST /dnc · scope campaigns:write · up to 1000 entries per request
| Field | Type | Required | Notes |
|---|---|---|---|
phone | string, 1 to 64 chars | Yes | Any format; matched on the trailing digits |
reason | string, up to 255 chars | No | Your note |
channel | string | No | all (default), voice, whatsapp or whatsapp_marketing |
curl -X POST https://api.callmissed.com/api/v1/voice-campaigns/dnc \
-H "Authorization: Bearer cm_your_api_key" \
-H "Content-Type: application/json" \
-d '{"entries": [
{"phone": "+919000000001", "reason": "asked not to be called"},
{"phone": "+919000000002", "channel": "whatsapp"}
]}'{ "added": 2, "skipped": 0 }Idempotent: a number already on the list for that channel is counted in skipped, not rejected.
Taking a number back off the list un-does somebody's opt-out, so it is done by an owner or admin in the console, not with an API key.
Errors
| Status | When |
|---|---|
403 | The API key is missing campaigns:read or campaigns:write |
404 | The campaign, contact, agent, number or voicemail template is not yours |
409 | The status move is not allowed, the campaign fails an enforced TRAI readiness check, or you are adding contacts to a running campaign |
422 | A DLT field with characters an identifier cannot have, a calling window that wraps midnight, a batch over the cap, a malformed column_map, or a CSV that is empty or too large |
Related
Call Handling API
Decide what happens when someone calls you: build call flows, press-1 menus and call queues, set the message left on an answering machine, and check whether a number has been flagged as spam — all with your cm_ key.
Caller ID pools
Let one outbound campaign dial from several of your own numbers. Create a pool, add numbers with optional daily limits, and attach it to a campaign.