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.

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

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,
  "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.

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, 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, "total_now": 2 }

A row with no usable digits is counted in skipped_invalid rather than failing the whole batch. This endpoint does not deduplicate — posting the same number twice queues two calls. Adding to a campaign that is running, completed or cancelled returns 409.

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 against the numbers already on it — so re-uploading the same file never queues a second call to the same person.

Form fieldTypeNotes
filefileUTF-8 CSV, first row is the header
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 'file=@leads.csv' \
  -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 and total_now. 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

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.

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.

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: no call is placed to a suppressed number from anywhere — a campaign, the Telephony API, an agent tool. Numbers are matched on their trailing digits, so the same person is suppressed whichever way the number was written.

GET /dnc · scope campaigns:read · limit 1–500 (default 100), offset 0–1000000

POST /dnc · scope campaigns:write · up to 1000 entries per request

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"}
      ]}'
{ "added": 1, "skipped": 0 }

Idempotent: a number already on the list is counted in skipped, not rejected.

DELETE /dnc/{entry_id} · scope campaigns:write · 204

Takes a number back off the list. Do this only when the person has asked to hear from you again.

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