Skip to main content

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.

An inbound call has to be answered by something. These five surfaces are that something: a flow (a step-by-step graph), a menu ("press 1 for sales"), a queue (hold until someone is free), a voicemail message for outbound calls that reach a machine, and a reputation check on the number itself.

Credential class: dashboard JWT or a cm_ key

Authorization: Bearer cm_your_api_key

Every endpoint on this page takes either. API-key callers need:

ActionScope
Listing, reading, simulating, live state, reputation checktelephony:read
Creating, updating, publishing, attaching a number, roster changes, deletingtelephony:write

Call handling is an attribute of telephony, so it reuses those two scopes and adds none. Base URL for every example: https://api.callmissed.com. Errors are {"detail": "..."}. Every create (POST on a collection) returns 201 Created, and every DELETE of a flow, menu, queue, member or voicemail message returns 204 No Content.

Flows, menus and queues can be built before calling is switched on for your account — they only read and write your own configuration. The voicemail-message and reputation endpoints need calling enabled and return 404 until it is.

How a number picks one

A number's binding is stored on the number itself, and each surface has its own attach call:

SurfaceAttachDetach
FlowPOST /api/v1/call-flows/{id}/numbers/{number_id}DELETE the same path
MenuPOST /api/v1/call-menus/{id}/numbers/{number_id}DELETE the same path
QueuePOST /api/v1/call-queues/{id}/numbers/{number_id}DELETE the same path

Attaching a flow or menu to a number is not yet available — inbound flow and menu execution is still being wired, so both attach calls return 409 for now. Flows and menus can still be created, published and simulated; detach stays live so an existing binding can always be cleared. Queue attach is unaffected and works today.

Detach is idempotent: unbinding a number that is not bound is a success, not an error. Deleting the flow, menu or queue a number points at is also safe — the call simply answers normally, because the binding is re-checked against your own rows on every call.

Both ids are checked against your account on every one of these calls, so a number can never be pointed at another account's flow, menu or queue.

Call flows

A flow is a directed graph of steps. Each step is a node with a type and a config; edges connect them.

The flow object

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "name": "Front desk",
  "description": "Business hours, then a menu",
  "graph": { "entry": "hello", "nodes": [], "edges": [] },
  "published_graph": { "entry": "hello", "nodes": [], "edges": [] },
  "version": 3,
  "published": true,
  "published_at": "2026-09-08T12:00:00Z",
  "created_at": "2026-09-01T09:00:00Z",
  "updated_at": "2026-09-08T11:59:00Z"
}
FieldNotes
graphThe draft. What you are editing
published_graphThe snapshot live calls actually walk. A copy taken at publish time, so editing the draft afterwards cannot change what callers hear
versionIncremented on every publish
publishedfalse means the flow answers nothing, even while published_graph is kept

Node types

typeWhat it doesconfigBranches it may declare in edges
greetingSays something and moves onprompt (≤ 2000 chars), prompt_audio_urlnext
dtmf_menuAsks for a keypress and routes on itprompt, prompt_audio_url, options, timeout_seconds (1–60, default 5), max_retries (0–5, default 2), invalid_action (reprompt or hangup)none — its branches live in options
business_hoursSplits on whether you are opentimezone (an IANA name), hours, holidays (≤ 60 YYYY-MM-DD dates)open, closed
routeTerminal: hands the call overdestinationnone
voicemailTakes a messageprompt, prompt_audio_url, max_duration_seconds (5–600, default 120)none
webhookCalls your endpoint mid-callurl (required), method (GET or POST), timeout_seconds (1–10), headers (≤ 10)ok, error

A destination — used by route.destination and by each key in a dtmf_menu's options — is {"type": ..., "value": ..., "label": ...}:

typevalue
botOne of your agents' ids
queueOne of your call queues' ids
phoneA number in E.164 form
menuInside a flow this means another NODE in the same flow. The id of that node, so a key hop stays in the graph
hangupNone

hours is keyed by mon tue wed thu fri sat sun, each a list of up to 6 {"start": "09:00", "end": "18:00"} windows. A day with no windows is closed all day. An unknown timezone, a malformed HH:MM, or a holiday that is not a real date is rejected at write time rather than silently misrouting a call later.

A webhook node's URL is checked when you save it and re-checked when the call fires it. Internal, loopback and link-local addresses are refused both times, so a flow cannot be used to reach something that is not on the public internet.

GET /api/v1/call-flows

Your flows, newest first. Requires telephony:read.

ParameterTypeNotes
limitint1–100, default 50
offsetint0–100000, default 0
curl https://api.callmissed.com/api/v1/call-flows \
  -H "Authorization: Bearer cm_your_api_key"

POST /api/v1/call-flows

Create a flow. Requires telephony:write. It answers nothing until it is published and a number is attached, so a half-built graph is safe to save.

FieldTypeNotes
namestringRequired. 1–255 characters
descriptionstring≤ 1024 characters
graphobjectentry, nodes (≤ 200), edges (≤ 600). Defaults to an empty graph
curl -X POST https://api.callmissed.com/api/v1/call-flows \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Front desk",
    "graph": {
      "entry": "hello",
      "nodes": [
        {"id": "hello", "type": "greeting", "config": {"prompt": "Thanks for calling Acme."}},
        {"id": "pick", "type": "dtmf_menu", "config": {
          "prompt": "Press 1 for sales, 2 for support.",
          "options": {
            "1": {"type": "bot", "value": "8f14e45f-ceea-467a-9f6b-0f9a7bd1c111", "label": "Sales"},
            "2": {"type": "queue", "value": "3d9a1b22-0f4c-4a61-9d33-3f2a51b0c222", "label": "Support"}
          }
        }}
      ],
      "edges": [{"from": "hello", "to": "pick", "on": "next"}]
    }
  }'

Structural problems are 422 with a message naming the node: a duplicate node id, an edge pointing at a node that does not exist, a branch the node type cannot take, or two branches of the same name leaving one node. A destination id that is not yours is 404.

entry may be omitted — the first node becomes the entry point — but an explicit one is clearer.

GET /api/v1/call-flows/{flow_id}

One flow, with both the draft and the published snapshot. Requires telephony:read.

PATCH /api/v1/call-flows/{flow_id}

Update the draft. Requires telephony:write. Live calls keep hearing the published snapshot until you publish again.

name, description and graph are all optional; an omitted field keeps its stored value and "description": "" clears it. graph is replaced wholesale, because a canvas is edited as one unit and a per-node merge would make deleting a node impossible. Send the whole graph.

POST /api/v1/call-flows/{flow_id}/publish

Snapshot the draft into published_graph, bump version, and start answering real calls with it. Requires telephony:write.

The draft is re-validated first — including re-checking that every destination still belongs to you — so a queue deleted since the flow was drawn is caught here instead of on a live call. A flow with no nodes, or whose entry node is missing, is refused with 422.

curl -X POST https://api.callmissed.com/api/v1/call-flows/{flow_id}/publish \
  -H "Authorization: Bearer cm_your_api_key"

POST /api/v1/call-flows/{flow_id}/unpublish

Stop answering with this flow. Requires telephony:write. The snapshot and the version are kept, so re-publishing is one call. Idempotent, and attached numbers answer normally in the meantime.

DELETE /api/v1/call-flows/{flow_id}

Delete the flow. Requires telephony:write. Numbers still carrying it answer normally from the next call.

POST /api/v1/call-flows/{flow_id}/simulate

Dry-run one step and get back the instruction a real call would receive. Requires telephony:read. Nothing is dialled and nothing is charged.

FieldTypeNotes
node_idstringWhere the call is. Omit to start at the entry node
eventstringstart, done or dtmf. Default start
pressed_keystringThe key pressed. Omit with dtmf to test a no-press timeout
attemptint0–6. 0 is the first ask, 1 the first reprompt
use_draftboolDefault true. false walks the published snapshot
atstringAn ISO timestamp to pretend it is — the way to test business_hours without waiting for 2am

Webhook nodes are not called during a simulation. Such a node takes its ok branch and the response lists it under simulated_webhook_nodes, so you never mistake a dry run for a real delivery.

curl -X POST https://api.callmissed.com/api/v1/call-flows/{flow_id}/simulate \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"event": "dtmf", "node_id": "pick", "pressed_key": "1"}'

Call menus

A standalone press-1-for-sales step, for when a whole flow is more than you need. Same idea as a flow's dtmf_menu node, but it is its own object and attaches to a number directly.

{
  "id": "3d9a1b22-0f4c-4a61-9d33-3f2a51b0c222",
  "name": "Main menu",
  "prompt": "Press 1 for sales, 2 for support.",
  "prompt_audio_url": null,
  "options": {
    "1": { "type": "bot", "value": "8f14e45f-ceea-467a-9f6b-0f9a7bd1c111", "label": "Sales" }
  },
  "timeout_seconds": 5,
  "invalid_action": "reprompt",
  "max_retries": 2,
  "enabled": true,
  "created_at": "2026-09-01T09:00:00Z",
  "updated_at": "2026-09-01T09:00:00Z"
}
EndpointScopeNotes
GET /api/v1/call-menustelephony:readlimit 1–100 (default 50), offset 0–100000
POST /api/v1/call-menustelephony:writename required
GET /api/v1/call-menus/{menu_id}telephony:read
PATCH /api/v1/call-menus/{menu_id}telephony:writeOmitted fields keep their value
DELETE /api/v1/call-menus/{menu_id}telephony:write
FieldTypeNotes
namestring1–255 characters
promptstring≤ 2000 characters. What the caller hears
prompt_audio_urlstringA public https recording to play instead of speaking the prompt. "" clears it
optionsobjectKeys "1" to "9", each {"type", "value", "label"} — the same destination shape as a flow, except that here menu means another call menu's id
timeout_secondsint1–60, default 5
invalid_actionstringreprompt or hangup. Default reprompt
max_retriesint0–5, default 2
enabledboolfalse keeps the menu but stops it answering

options is replaced wholesale on PATCH, so send every key you want to keep. A menu pointing at itself is refused (422) — it would loop the caller forever. A key outside 1–9, or a destination missing its value, is also 422; a destination id that is not yours is 404.

curl -X POST https://api.callmissed.com/api/v1/call-menus \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Main menu",
    "prompt": "Press 1 for sales, 2 for support.",
    "options": {
      "1": {"type": "bot", "value": "8f14e45f-ceea-467a-9f6b-0f9a7bd1c111", "label": "Sales"},
      "2": {"type": "queue", "value": "3d9a1b22-0f4c-4a61-9d33-3f2a51b0c222", "label": "Support"}
    }
  }'

Call queues

A queue holds callers until someone on its roster is free.

{
  "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "name": "Support",
  "strategy": "round_robin",
  "hold_audio_url": null,
  "max_wait_seconds": 300,
  "overflow_action": "voicemail",
  "enabled": true,
  "created_at": "2026-09-01T09:00:00Z",
  "updated_at": "2026-09-01T09:00:00Z"
}
FieldTypeNotes
namestring1–255 characters
strategystringround_robin, longest_idle or priority. Default round_robin
hold_audio_urlstringA public https clip played while holding. "" clears it
max_wait_secondsint0–3600, default 300. 0 means hold indefinitely
overflow_actionstringvoicemail, hangup or callback. What happens after max_wait_seconds
enabledboolfalse keeps the queue but stops it taking calls
EndpointScopeNotes
GET /api/v1/call-queuestelephony:readlimit 1–100 (default 50), offset 0–100000
POST /api/v1/call-queuestelephony:writename required
GET /api/v1/call-queues/{queue_id}telephony:read
PATCH /api/v1/call-queues/{queue_id}telephony:writeOmitted fields keep their value
DELETE /api/v1/call-queues/{queue_id}telephony:writeRoster and anyone waiting go with it

The roster

A member is either an agent (kind: "ai", bot_id) or a person in your workspace (kind: "human", user_id). Either way the id must belong to your account, which is what stops a queue routing calls to somewhere else.

EndpointScopeNotes
GET /api/v1/call-queues/{queue_id}/memberstelephony:readlimit 1–500 (default 100), offset 0–100000
POST /api/v1/call-queues/{queue_id}/memberstelephony:write
PATCH /api/v1/call-queues/{queue_id}/members/{member_id}telephony:write
DELETE /api/v1/call-queues/{queue_id}/members/{member_id}telephony:write
FieldTypeNotes
kindstringai (default) or human
bot_iduuidRequired when kind is ai
user_iduuidRequired when kind is human, and the user must be active
priorityint0–1000, default 100. Lower is served sooner
max_concurrentint1–50, default 1. How many calls at once
enabledboolfalse keeps them on the roster and sends them nothing

PATCH changes priority, max_concurrent and enabled only — kind, bot_id and user_id are immutable, so point a queue at a different answerer by removing the member and adding a new one. Raising max_concurrent or re-enabling a member pulls waiting callers in straight away rather than at the next release.

curl -X POST https://api.callmissed.com/api/v1/call-queues/{queue_id}/members \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"kind": "ai", "bot_id": "8f14e45f-ceea-467a-9f6b-0f9a7bd1c111", "max_concurrent": 3}'

GET /api/v1/call-queues/{queue_id}/live

Who is holding right now. Requires telephony:read. limit is 1–200, default 50.

{
  "queue_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "strategy": "round_robin",
  "waiting": 2,
  "members_total": 4,
  "members_free": 1,
  "longest_wait_seconds": 96,
  "callers": [
    {
      "entry_id": "1f0c2e77-1b2a-4a55-8f0e-9b1c2d3e4f50",
      "caller_suffix": "…4321",
      "position": 1,
      "waiting_seconds": 96,
      "priority": 100,
      "enqueued_at": "2026-09-08T12:00:00Z"
    }
  ]
}

A caller's full number is never returned here — only the last four digits, which is enough to match someone on screen. This is a point-in-time read; poll it no faster than you need.

Voicemail messages

What an outbound call leaves when an answering machine picks up. Needs calling enabled on your account.

{
  "id": "2f8a7c10-5b6d-4e3f-8a1b-7c9d0e1f2a3b",
  "tenant_id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
  "name": "Missed you — sales",
  "text": "Hi, this is Acme returning your call. We'll try again tomorrow.",
  "voice": null,
  "language": "en-IN",
  "audio_state": "pending",
  "created_at": "2026-09-01T09:00:00Z",
  "updated_at": "2026-09-01T09:00:00Z"
}
FieldTypeNotes
namestring1–255 characters
textstring≤ 2500 characters. The words to leave
voicestring≤ 64 characters. Which voice reads it; omit for the account default
languagestring≤ 16 characters, e.g. en-IN
audio_statestringready (a clip exists), pending (text only — it is turned into speech, and billed, the first time it is used) or empty (nothing to leave)
EndpointScopeNotes
GET /api/v1/voicemail-templatestelephony:readlimit 1–100 (default 50), offset 0–100000
POST /api/v1/voicemail-templatestelephony:writename required
GET /api/v1/voicemail-templates/{template_id}telephony:read
PATCH /api/v1/voicemail-templates/{template_id}telephony:writeOmitted fields keep their value
DELETE /api/v1/voicemail-templates/{template_id}telephony:writeAlso removes the cached clip
GET /api/v1/voicemail-templates/{template_id}/audiotelephony:read{"url": "..."} — a short-lived link. A text-only message is synthesised, cached and billed once here, so a preview costs the same as the first real drop
POST /api/v1/voicemail-templates/{template_id}/audiotelephony:writemultipart/form-data with file: a WAV or MP3 up to 5 MB. Bypasses synthesis entirely, so a professionally recorded greeting is used as-is and nothing is billed for speech

Changing text throws away the clip made from the old words, and the next use re-synthesises once. Without that, a call would keep playing the old message forever.

Number reputation

Whether one of your numbers has been reported as spam, and what is still open against it. Needs calling enabled on your account.

GET /api/v1/reputation/{number}

Requires telephony:read. {number} is one of your numbers in E.164, with or without the leading +. A number you do not own returns 404 — never 403 — so nobody can probe for other accounts' numbers.

{
  "phone_number": "919000000000",
  "status": "pending",
  "complaints": [
    {
      "reference_id": "PUCC-000123",
      "status": "pending",
      "to_number": "919111111111",
      "initiation_date": "2026-09-05",
      "complaint_category": "promotional",
      "opt_in_proof": null,
      "rejection_reason": null,
      "actionable": true
    }
  ],
  "actionable_count": 1,
  "last_checked": "2026-09-08T11:30:00Z",
  "stale": false
}
FieldNotes
statusThe worst open standing: clean, pending, in_review, rejected or accepted
actionable / actionable_countComplaints that still need proof from you
to_numberThe complainant's own number. You need it on the proof document, and you must stop calling them — continuing to call a complainant is itself a violation
staletrue when the standing could not be refreshed recently, or has never been checked. Treat a clean answer with stale: true as unknown, not clean

An open complaint has a 5-business-day window to answer in. Missing it risks the number being blocked across India for a year, so check actionable_count on a schedule rather than after a problem.

POST /api/v1/reputation/{number}/remediate

Upload opt-in proof for one complaint. Requires telephony:write. multipart/form-data:

PartNotes
reference_idThe complaint's reference_id, ≤ 64 characters
filePDF, PNG or JPEG, up to 10 MB
curl -X POST https://api.callmissed.com/api/v1/reputation/919000000000/remediate \
  -H "Authorization: Bearer cm_your_api_key" \
  -F 'reference_id=PUCC-000123' \
  -F 'file=@opt-in-proof.pdf'
{
  "phone_number": "919000000000",
  "reference_id": "PUCC-000123",
  "status": "in_review",
  "message": "Opt-in proof uploaded and the complaint is under review. ..."
}

A file over 10 MB returns 413. Only the file's type and size are checked at upload; the document itself is reviewed afterwards, so a 200 means "under review", not "cleared". The proof must show your business logo, the opt-in date (within the six months before the complaint) and the complainant's exact number, or the review declines it.

Do this from an AI assistant instead

Every read and write on this page except the two file uploads is also a tool on the Account MCP Server, so an assistant you have connected can build a flow, wire a menu, staff a queue and publish the lot without you writing a single request.