Skip to main content

Calling

Voice calls over WhatsApp: enable calling, request permission, place a business-initiated call answered by your agent, and read call logs with transcripts.

WhatsApp Calling lets a customer call your business number, and lets you call them, over WhatsApp itself rather than the phone network. Calls are answered by the same agent that handles the number's messages, with the same voice, model and system prompt, so a call produces a transcript and per-turn AI cost exactly like any other voice session.

All endpoints are under https://api.callmissed.com/api/v1/whatsapp.

Before you can call

Three things have to be true, and each has its own failure code:

  1. Calling is enabled on the number. Turn it on with call settings, or in WhatsApp Manager. Otherwise sends fail with 409 telling you calling is not enabled.
  2. The number's messaging limit is 2000 or above. WhatsApp requires it. Below that you get 409.
  3. The customer granted call permission. Inbound calls need nothing, but a business-initiated call is permission-gated and returns 403 without one.

Call settings

Read settings

GET /api/v1/whatsapp/calling/settings · scope whatsapp:read

Query paramTypeRequiredNotes
phone_id / phone_number_idUUID / stringOne ofThe number
curl "https://api.callmissed.com/api/v1/whatsapp/calling/settings?phone_number_id=1234567890" \
  -H "Authorization: Bearer cm_your_api_key"

Returns WhatsApp's own settings object for the number, unchanged.

Update settings

POST /api/v1/whatsapp/calling/settings · scope whatsapp:write

Send only the fields you want to change. At least one is required, or you get 400 with "No calling settings provided".

FieldTypeRequiredNotes
phone_id / phone_number_idUUID / stringOne ofThe number
statusstringNoENABLED or DISABLED. The master switch for calling on this number
call_icon_visibilitystring, max 32NoWhere WhatsApp shows the call button
callback_permission_statusstringNoENABLED or DISABLED
call_hoursobjectNoYour calling hours, forwarded to WhatsApp unchanged
sipobjectNoSIP configuration, forwarded unchanged
audioobjectNoAudio configuration, forwarded unchanged
voicemailobjectNoVoicemail configuration, forwarded unchanged
curl -X POST https://api.callmissed.com/api/v1/whatsapp/calling/settings \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1234567890",
    "status": "ENABLED",
    "callback_permission_status": "ENABLED"
  }'

The nested objects are passed to WhatsApp exactly as you send them and validated there, so any option WhatsApp supports works without waiting on a CallMissed release. Returns WhatsApp's response.

Call permission

WhatsApp requires an explicit grant from the customer before a business may call them. A grant is temporary or permanent; temporary grants expire, so re-check before relying on one.

Check one user's permission

GET /api/v1/whatsapp/calling/permissions · scope whatsapp:read

Query paramTypeRequiredNotes
userstring, 5 to 20 charsYesThe customer's WhatsApp id
phone_id / phone_number_idUUID / stringOne ofYour number
curl "https://api.callmissed.com/api/v1/whatsapp/calling/permissions?phone_number_id=1234567890&user=919000000000" \
  -H "Authorization: Bearer cm_your_api_key"

Returns WhatsApp's permission object, which carries a permission.status of no_permission, temporary or permanent. The result is also recorded locally, so a number that has granted permission shows up in allowed numbers even before you call it.

Ask for permission

POST /api/v1/whatsapp/calling/permission-request · scope whatsapp:send

Sends the customer an interactive message asking them to allow calls. It only works inside an open 24-hour customer service window. Outside it, send an approved call_permission_request template instead.

FieldTypeRequiredNotes
phone_id / phone_number_idUUID / stringOne ofYour number
tostring, 5 to 20 charsYesThe customer in E.164
body_textstring, 1 to 1024 charsYesWhy you want to call
curl -X POST https://api.callmissed.com/api/v1/whatsapp/calling/permission-request \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1234567890",
    "to": "+919000000000",
    "body_text": "Can we call you about order AC-10294? It will take about two minutes."
  }'

Response (200 OK)

{ "wamid": "wamid.HBgMOTE5MDAwMDAwMDAwFQIAERgSQjE1RDNBOEY0RTVCOTAxMgA=" }

The customer's answer arrives as an inbound event, and their permission state is reflected on the next permission check.

List allowed numbers

GET /api/v1/whatsapp/calling/allowed-numbers · scope whatsapp:read

Numbers that have granted call permission, so you can pick one and dial. WhatsApp exposes no bulk lookup, so this is derived from the permission state recorded whenever you check permission, request it, or place a call. Deduplicated per number, most recent first.

Query paramTypeDefaultNotes
phone_idUUIDnoneRestrict to one of your numbers
limitinteger, 1 to 500100Max rows
curl "https://api.callmissed.com/api/v1/whatsapp/calling/allowed-numbers?limit=100" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

[
  {
    "to_number": "919000000000",
    "permission_status": "permanent",
    "last_call_at": "2026-04-19T12:31:08Z"
  }
]
FieldTypeNotes
to_numberstringThe customer's number
permission_statusstringtemporary or permanent
last_call_atdatetime, nullableWhen we last saw this number

A temporary grant expires. Re-check with the permissions endpoint before relying on one.

Place a call

POST /api/v1/whatsapp/calling/initiate · scope whatsapp:send

Places a business-initiated call. Permission is verified first, the credits are held, the media bridge is provisioned, and the agent picks up when the customer answers.

FieldTypeRequiredNotes
phone_id / phone_number_idUUID / stringOne ofThe number to call from
tostring, 5 to 20 charsYesThe customer in E.164
reasonstring, max 512NoYour own note on why the call was placed. Stored on the call log, not sent to WhatsApp
curl -X POST https://api.callmissed.com/api/v1/whatsapp/calling/initiate \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "1234567890",
    "to": "+919000000000",
    "reason": "Delivery address could not be verified for AC-10294"
  }'

Response (200 OK)

{
  "id": "c4a7f210-3b8e-4d1f-9a2c-5e6b7d8f9012",
  "session_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
  "status": "INITIATED"
}
FieldTypeNotes
idUUIDThe CallMissed call row
session_idUUIDThe linked voice session, where the transcript and AI cost land
statusstringAlways INITIATED at this point

WhatsApp's own call_id does not exist yet. It is minted moments later as the call is placed, and appears on the call log once the handshake completes. Correlate by session_id until then.

Failures

CodeMeaning
402Not enough credits to cover the call. Nothing was placed
403The customer has not granted call permission. Send a permission request first
404The calling number is not on your workspace
409Calling is not enabled on the number, or its messaging limit is below 2000
503The calling media bridge is unavailable right now

The credit hold

The network leg is charged when the call ends, so an unfundable call cannot be undone once placed. Before the call is provisioned, its worst-case cost is held: the agent's own maximum call duration, priced at the recipient's regional per-minute rate. A shortfall returns 402 and nothing is placed, no room is created and WhatsApp is never asked to dial.

Over-holding is self-correcting. When the call ends, the real charge settles and the remainder is released. Inbound calls are not held at all, because WhatsApp does not charge for user-initiated calls.

Call logs

List calls

GET /api/v1/whatsapp/calling/calls · scope whatsapp:read

Most recent first.

Query paramTypeDefaultNotes
phone_idUUIDnoneRestrict to one of your numbers
limitinteger, 1 to 20050Max rows
offsetinteger, 0 to 1000000Pagination offset
curl "https://api.callmissed.com/api/v1/whatsapp/calling/calls?limit=50&offset=0" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

[
  {
    "id": "c4a7f210-3b8e-4d1f-9a2c-5e6b7d8f9012",
    "call_id": "wacid.HBgMOTE5MDAwMDAwMDAwFQIAERgSQTBGOEQ3MTJGM0EyRDFDNQA=",
    "direction": "BUSINESS_INITIATED",
    "status": "COMPLETED",
    "from_wa_id": "1234567890",
    "to_number": "919000000000",
    "duration_seconds": 96,
    "cost_credits": 4.8,
    "voice_session_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
    "permission_status": "permanent",
    "created_at": "2026-04-19T12:31:08Z",
    "ai_cost_credits": 2.1374
  }
]
FieldTypeNotes
idUUIDThe CallMissed call row
call_idstringWhatsApp's call id. Use it on the detail and terminate endpoints
directionstringBUSINESS_INITIATED or USER_INITIATED
statusstringLifecycle state, for example INITIATED, RINGING, ACCEPTED, COMPLETED, TERMINATED, FAILED. Stored as WhatsApp reports it, so new values can appear
from_wa_idstring, nullableThe calling side
to_numberstring, nullableThe called side
duration_secondsinteger, nullableCall length. Falls back to the voice session's duration when the call was ended by the agent
cost_creditsfloat, nullableThe WhatsApp network leg only. Zero for inbound calls, which WhatsApp does not charge for
voice_session_idUUID, nullableThe linked voice session
permission_statusstring, nullableThe permission snapshot when the call was placed
created_atdatetime, nullableISO 8601 UTC
ai_cost_creditsfloat, nullableSpeech, model and voice cost for the call. Separate from cost_credits

Permission-check marker rows are excluded, so this list is real calls only.

Get one call with its transcript

GET /api/v1/whatsapp/calling/calls/{call_id} · scope whatsapp:read

{call_id} is WhatsApp's call id. Only A-Z a-z 0-9 _ . : = - are accepted in the path, up to 128 characters.

curl "https://api.callmissed.com/api/v1/whatsapp/calling/calls/wacid.HBgMOTE5MDAwMDAwMDAwFQIAERgSQTBGOEQ3MTJGM0EyRDFDNQA=" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

{
  "id": "c4a7f210-3b8e-4d1f-9a2c-5e6b7d8f9012",
  "call_id": "wacid.HBgMOTE5MDAwMDAwMDAwFQIAERgSQTBGOEQ3MTJGM0EyRDFDNQA=",
  "direction": "BUSINESS_INITIATED",
  "status": "COMPLETED",
  "from_wa_id": "1234567890",
  "to_number": "919000000000",
  "duration_seconds": 96,
  "cost_credits": 4.8,
  "voice_session_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
  "permission_status": "permanent",
  "created_at": "2026-04-19T12:31:08Z",
  "ai_cost_credits": 2.1374,
  "transcript": [
    {
      "turn_index": 0,
      "user_transcript": null,
      "agent_response": "Hi, this is Acme Coffee calling about order AC-10294.",
      "interrupted": false
    },
    {
      "turn_index": 1,
      "user_transcript": "Yes, go ahead.",
      "agent_response": "We could not verify the delivery address. Is flat 4B still correct?",
      "interrupted": false
    }
  ]
}

Every field from the list response, plus:

FieldTypeNotes
transcript[].turn_indexintegerTurn order, starting at 0
transcript[].user_transcriptstring, nullableWhat the customer said
transcript[].agent_responsestring, nullableWhat the agent said
transcript[].interruptedbooleanWhether the customer spoke over the agent

transcript is empty until the agent has persisted turns, so it is normally empty while a call is still running. 404 if the call is not on your workspace.

Terminate a live call

POST /api/v1/whatsapp/calling/calls/{call_id}/terminate · scope whatsapp:send

No body. Hangs up on WhatsApp's side and immediately marks the local row TERMINATED, so your call log updates without waiting for the webhook.

curl -X POST "https://api.callmissed.com/api/v1/whatsapp/calling/calls/wacid.HBgMOTE5MDAwMDAwMDAwFQIAERgSQTBGOEQ3MTJGM0EyRDFDNQA=/terminate" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

{ "success": true }

404 if the call is not on your workspace.