Skip to main content

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. 1

    Create

    makes a draft. Nobody is called yet

  2. 2

    Load the list

    or a CSV upload

  3. 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.

FromMay move to
draftscheduled, cancelled
scheduledrunning, paused, draft, cancelled
runningpaused, completed, cancelled
pausedrunning, scheduled, cancelled
insufficient_balancerunning, 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

FieldTypeRequiredNotes
namestring (1–255)YesYour own label for the campaign
bot_iduuidNoThe voice agent that answers for you on each call
from_number_iduuidNoWhich of your numbers to call from
caller_id_pool_iduuidNoDial from a caller ID pool instead. While the pool is enabled it replaces from_number_id. Send null on a patch to detach
scheduled_attimestampNoWhen the campaign should begin
timezonestringNoIANA zone the calling hours are read in. Default Asia/Kolkata
quiet_hours_startinteger 0–23NoEarliest local hour a call may be placed. Default 9
quiet_hours_endinteger 1–24NoLatest local hour. Must be after the start. Default 21
max_attemptsinteger 1–5NoRetries per contact. Default 3
dial_modepreview | power | parallelNoDefault power
pace_secondsinteger 5–3600NoSeconds between calls in power mode. Default 30
parallel_linesinteger 1–5NoCalls at once in parallel mode. Default 1
amd_enabledbooleanNoDetect an answering machine. Off by default
amd_modeclassic | full_greeting | offNoHow 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_iduuidNoVoicemail to leave on a machine. Omit to detect and hang up
dlt_principal_entity_idstring (≤32, letters/digits)NoYour India DLT principal entity ID. See TRAI readiness
dlt_headerstring (≤32)NoThe DLT header / caller ID these calls go out under
dlt_template_idstring (≤64)NoDLT content template ID, if you registered one
dlt_consent_refstring (≤128)NoYour 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

ModeWhat it doesUse it when
previewNothing is dialled until a person approves each contact, one at a timeA human should see who is next before the phone rings
powerOne call at a time, pace_seconds apartThe default. Steady, predictable pacing
parallelUp to parallel_lines calls at once; the first answer winsLong 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_modeHow it decidesWhen the drop plays
classic (default)The callee's first words are matched against known voicemail and network greetings in English, Hindi and HinglishAs soon as a greeting phrase is recognised
full_greetingListens to the whole greeting and classifies it with your agent's language model. A short spoken "hello" counts as a person straight awayAfter the greeting ends and the record beep has sounded
offNo detection, even with amd_enabled onNever

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

FieldTypeRequired
contacts[].phonestring (1–64)Yes
contacts[].namestring (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 fieldTypeNotes
filefileCSV, first row is the header. UTF-8 or Windows-1252; comma, semicolon or tab separated
column_mapstringJSON object mapping your headers to our fields, e.g. {"Mobile No.":"phone"}
dry_runbooleanDefault true — previews both effects and writes nothing
opted_inbooleanAsserts 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:

FieldNotes
statuspending, approved, calling, done or failed
attemptsHow many times it has been tried
next_attempt_atWhen the next retry is due, if any
last_errorWhy the last attempt did not complete
call_idThe 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_id and dlt_header are set.
  • Either dlt_template_id or dlt_consent_ref is 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. ..." }
  ]
}
codeMeaning
no_from_numberNo active number to call from
number_not_declaredThe calling number has no A2P declaration recorded
dlt_principal_entity_missingdlt_principal_entity_id is empty
dlt_header_missingdlt_header is empty
dlt_consent_or_template_missingNeither 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:

channelBlocks
all (default)Calls and every WhatsApp message
voiceCalls only
whatsappEvery WhatsApp message, not calls. Added automatically when a customer sends STOP
whatsapp_marketingWhatsApp 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

FieldTypeRequiredNotes
phonestring, 1 to 64 charsYesAny format; matched on the trailing digits
reasonstring, up to 255 charsNoYour note
channelstringNoall (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

StatusWhen
403The API key is missing campaigns:read or campaigns:write
404The campaign, contact, agent, number or voicemail template is not yours
409The status move is not allowed, the campaign fails an enforced TRAI readiness check, or you are adding contacts to a running campaign
422A 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