Skip to main content

Telephony API

Complete India KYC, rent Indian phone numbers, link them to a voice-agent bot, place, bridge and hang up outbound PSTN calls, and fetch recordings — all with your cm_ key.

Overview

The Telephony API is a full lifecycle for CallMissed Numbers: submit an India KYC (compliance) application, wait for it to be accepted, search available Indian numbers, buy one (a paid action that draws your real credit balance), manage it, and place outbound PSTN calls answered by your AI voice agent.

Base path: https://api.callmissed.com/api/v1/telephony

The journey is ordered. You cannot buy a number until you hold an accepted KYC application, and you cannot place a call until you own an active number. Follow the flow below top to bottom.

  1. 1

    Submit KYC

    Upload your business documents and details once

  2. 2

    CallMissed

    Reviews the application; poll or sync until it is

  3. 3

    Buy & call

    Search a number, buy it (paid), link a bot, place calls

Authentication. Every endpoint accepts both a JWT (Authorization: Bearer <jwt>) and an API key (Authorization: Bearer cm_<key>). API-key callers need the telephony:read scope for search/list/get and telephony:write for every write: buy, release, patch, the AI-call declaration, KYC submit/sync, placing, bridging and hanging up calls, and voicemail drops. Those writes additionally require an owner/admin role when called with a JWT.

Availability. Telephony is India-only and enabled per tenant. When the feature is not enabled for your tenant, the routes are unmounted and every call returns 404.

1. Submit KYC (Compliance)

Every rented Indian number must be backed by an accepted KYC application. Submission is a multipart form carrying your business details plus the two mandatory documents:

  • Registration certificate — Certificate of Incorporation (CIN) or Udyam certificate
  • GST certificate

Files must be PDF, JPEG, or PNG, up to 5 MB each. The legal business name must match exactly on both documents or the application is rejected upstream.

POST /compliance · scope telephony:write (owner/admin for JWT)

Form fields:

FieldTypeRequiredNotes
aliasstring (1–128)YesA label for this application
business_namestring (1–100)YesLegal name, exactly as printed on both documents
registration_numberstring (1–64)YesCIN or Udyam number
emailstring (3–254)YesBusiness contact email
address_line1string (1–255)Yes
address_line2string (0–255)No
citystring (1–100)Yes
statestring (1–100)Yes
postal_codestring (1–16)Yes
registration_certfileYesCOI or Udyam certificate (PDF/JPEG/PNG, ≤ 5 MB)
gst_certfileYesGST certificate (PDF/JPEG/PNG, ≤ 5 MB)
curl -X POST https://api.callmissed.com/api/v1/telephony/compliance \
  -H "Authorization: Bearer cm_your_api_key" \
  -F 'alias=Acme India KYC' \
  -F 'business_name=ACME TECHNOLOGIES PRIVATE LIMITED' \
  -F 'registration_number=U72900KA2020PTC000000' \
  -F 'email=compliance@acme.in' \
  -F 'address_line1=123 MG Road' \
  -F 'address_line2=Suite 400' \
  -F 'city=Bengaluru' \
  -F 'state=Karnataka' \
  -F 'postal_code=560001' \
  -F 'registration_cert=@certificate-of-incorporation.pdf' \
  -F 'gst_cert=@gst-certificate.pdf'

Response (200 OK) — a compliance application:

{
  "id": "3f9a1c20-7d8e-4b1a-9c2f-5e6a7b8c9d0e",
  "tenant_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "alias": "Acme India KYC",
  "country_iso": "IN",
  "number_type": "local",
  "user_type": "business",
  "status": "submitted",
  "plivo_compliance_id": null,
  "rejection_reason": null,
  "business_name": "ACME TECHNOLOGIES PRIVATE LIMITED",
  "registration_number": "U72900KA2020PTC000000",
  "created_at": "2026-04-19T12:00:00Z"
}

Status codes

CodeMeaning
200Application created and submitted
403Missing telephony:write scope, or JWT caller is not an owner/admin
413A document exceeds the 5 MB limit
422A document is missing, empty, or not a PDF/JPEG/PNG
404Telephony not enabled for your tenant

2. Check KYC Status

Applications move through draft → submitted → accepted / rejected, and an accepted application can later become suspended or expired. Only an accepted application can back a number purchase.

EndpointScopePurpose
GET /compliancetelephony:readList your applications
GET /compliance/{application_id}telephony:readGet one application + status
POST /compliance/{application_id}/synctelephony:write (owner/admin for JWT)Refresh status from the carrier

GET /compliance accepts limit (1–200, default 50) and offset (≥ 0). It returns an array of applications, newest first. POST /compliance/{application_id}/sync pulls the latest status and, if rejected, populates rejection_reason.

# List applications
curl https://api.callmissed.com/api/v1/telephony/compliance \
  -H "Authorization: Bearer cm_your_api_key"

# Refresh one application's status
curl -X POST https://api.callmissed.com/api/v1/telephony/compliance/{application_id}/sync \
  -H "Authorization: Bearer cm_your_api_key"

Once status is accepted, the application's plivo_compliance_id field holds the compliance reference you pass as compliance_application_id when buying a number (step 4). Note this is that field's value, not the application's id. A GET/sync on an application you do not own returns 404.

3. Search Available Numbers

Search for Indian numbers before buying. Rates are returned after your tenant markup — rental_credits is what you will actually be charged per month.

GET /numbers/search · scope telephony:read

Query parameters

ParamValuesDefault
country_iso2-letter ISO (IN)IN
typelocal / mobile / tollfree—
patterndigit substring to match (max 32 chars)—
limit1–2020
curl "https://api.callmissed.com/api/v1/telephony/numbers/search?country_iso=IN&type=local&pattern=80802&limit=10" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK) — an array of search hits:

[
  {
    "number": "+918080247309",
    "number_type": "local",
    "country": "IN",
    "region": "Mumbai",
    "monthly_rental_rate_usd": 2.5,
    "rental_credits": 250,
    "voice_enabled": true,
    "sms_enabled": false
  }
]

4. Buy a Number

Rent one of the searched numbers. This is a paid action — it draws your real (paid) credit balance. The signup bonus does not cover a number rental; if your paid balance is short, you get a 402 telling you to top up.

POST /numbers · scope telephony:write (owner/admin for JWT)

Request body

FieldTypeRequiredNotes
e164stringYesThe number to buy, e.g. +918080247309
compliance_application_idstringYesThe plivo_compliance_id of your accepted KYC application

The purchase runs in a strict, money-safe order: it verifies your KYC application is accepted (else 409), confirms the number is still available and prices it live (else 422), deducts the rental credits, and only then rents the number. If the rent fails, the credits are refunded automatically.

curl -X POST https://api.callmissed.com/api/v1/telephony/numbers \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "e164": "+918080247309",
    "compliance_application_id": "your-accepted-compliance-reference"
  }'

Response (200 OK) — the rented number:

{
  "id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
  "tenant_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "e164": "+918080247309",
  "country_iso": "IN",
  "number_type": "local",
  "status": "active",
  "bot_id": null,
  "alias": null,
  "monthly_rental_rate_usd": 2.5,
  "rental_credits": 250,
  "added_on": "2026-04-19",
  "renewal_date": "2026-05-19",
  "config": null,
  "metadata": null,
  "a2p_declared_at": null,
  "a2p_series": null,
  "a2p_tsp_reference": null,
  "created_at": "2026-04-19T12:00:00Z"
}

Status codes

CodeMeaning
200Number rented and active
402Insufficient real balance — top up to buy
403Missing telephony:write scope, or JWT caller is not an owner/admin
409KYC not accepted, or you already hold this number
422Number no longer available, or e164 is malformed
404Telephony not enabled for your tenant

5. Manage Numbers

EndpointScopePurpose
GET /numberstelephony:readList your rented numbers
GET /numbers/{number_id}telephony:readGet one number
PATCH /numbers/{number_id}telephony:write (owner/admin for JWT)Update alias / linked bot / per-number call config
DELETE /numbers/{number_id}?confirm=truetelephony:write (owner/admin for JWT)Release a number (permanent)

GET /numbers accepts status (e.g. active), limit (1–200, default 50), and offset (0–100000), newest first. PATCH updates only the fields you send; a bot_id that is not yours returns 404. A number moves through pending → active → suspended (unpaid) → released.

# List your rented numbers
curl https://api.callmissed.com/api/v1/telephony/numbers \
  -H "Authorization: Bearer cm_your_api_key"

# Get one
curl https://api.callmissed.com/api/v1/telephony/numbers/{number_id} \
  -H "Authorization: Bearer cm_your_api_key"

# Update the alias and link a voice-agent bot
curl -X PATCH https://api.callmissed.com/api/v1/telephony/numbers/{number_id} \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"alias": "Support line", "bot_id": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"}'

Per-number call overrides

The optional config object holds per-number call-handling overrides that win over the linked bot's config on this number's calls — so two numbers can share one bot yet greet and speak differently. The object replaces the stored overrides on every write (send the full set each time; {} clears every override). A key set to null or "" is dropped, so the bot's own value applies again. Unknown keys return 422.

KeyTypeNotes
voice_modelstring (≤ 100)Voice LLM model id
voicestring (≤ 64)Voice / speaker id
languagestring (≤ 16)e.g. hi-IN
stt_modelstring (≤ 64)Speech-to-text model id
tts_modelstring (≤ 64)Text-to-speech model id
tts_providerstring (≤ 32)TTS provider id
tts_enginestring (≤ 32)TTS engine id
greetingstring (≤ 500)Opening line spoken on the call
system_promptstring (≤ 8000)Overrides the bot's persona for this number
max_call_duration_secondsinteger30–14400
allow_interruptionsbooleanfalse turns barge-in off on this number, e.g. for a scripted disclosure
toolsarray of stringsUp to 20 agent tool names
queue_idstringThe call queue this number routes through. Normally set by attaching a queue; keep it in the set you send, or the binding is cleared
menu_id / flow_idstringAccepted only as null or "", to clear an old binding. Any other value returns 422, because inbound menus and flows are not available yet
curl -X PATCH https://api.callmissed.com/api/v1/telephony/numbers/{number_id} \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"config": {"greeting": "Namaste! Aap Support line par pahunche hain.", "language": "hi-IN", "max_call_duration_seconds": 600}}'

Release a number

curl -X DELETE "https://api.callmissed.com/api/v1/telephony/numbers/{number_id}?confirm=true" \
  -H "Authorization: Bearer cm_your_api_key"

confirm=true is required — releasing a number is permanent, stops its monthly rental charge, and is not refunded. Requires telephony:write (owner/admin for JWT). Returns 204 No Content on success, 400 if confirm is omitted, and 404 if the number is not found or not owned by your tenant.

Declare a number for AI calls (India)

TRAI requires a business making automated or AI calls to declare that use, and the caller IDs it uses, to its telecom provider first (press release 119/2026). After you have made that declaration with your provider, record it on the number. Campaigns check it before they dial; see TRAI readiness.

curl -X PUT https://api.callmissed.com/api/v1/telephony/numbers/{number_id}/a2p-declaration \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"series": "1600", "tsp_reference": "DECL-2026-0042", "confirm_declared_to_tsp": true}'
FieldTypeRequiredNotes
series140 | 1600 | 1601 | standardYes140, 1600 and 1601 are TRAI's commercial-call series; standard is an ordinary number
tsp_referencestring (≤64)NoYour telecom provider's reference for the declaration
confirm_declared_to_tspbooleanYesMust be true. You confirm the declaration was made to your provider

Returns the number with a2p_declared_at, a2p_series and a2p_tsp_reference set. Declaring again updates them. confirm_declared_to_tsp: false returns 422, and so does a tsp_reference with characters other than letters, digits and _ . / -. DELETE on the same path withdraws the declaration and returns the number. Both need telephony:write (owner/admin for JWT). A released number returns 409.

6. Place a Call

Originate an outbound PSTN call from one of your active numbers. Link a bot_id to have your AI voice agent handle the call; if you omit it, the number's persistently bound bot is used.

POST /calls · scope telephony:write (owner/admin for JWT)

Request body

FieldTypeRequiredNotes
from_number_idUUIDYesAn active number you own
to_e164stringYesThe destination, e.g. +919000000000
bot_idUUIDNoVoice-agent bot to answer the call
reasonstring (≤ 500)NoPlain-language purpose; spoken on the outbound greeting
greeting_modeoutbound | inboundNoDefault follows the call: the agent introduces itself and states reason. inbound makes it greet in character from its own greeting, as it would answer an inbound caller, and not speak reason (still stored on the call). Useful for test calls to yourself
variablesobjectNoPer-call {{token}} values, merged over the agent's declared input-variable defaults and rendered into its greeting and prompt. Up to 50 entries; keys match ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$; each value ≤ 200 characters
status_callback_urlstring (≤ 2048)NoA public https URL we POST this call's lifecycle payload to on every status change — the same shape as the account-level call.* webhook events. Internal or private addresses are refused with 400
curl -X POST https://api.callmissed.com/api/v1/telephony/calls \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from_number_id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
    "to_e164": "+919000000000",
    "bot_id": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
    "reason": "Confirming your appointment for tomorrow"
  }'

Response (200 OK) — the created call (status advances via webhooks):

{
  "id": "5e6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "tenant_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "phone_number_id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
  "bot_id": "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "voice_session_id": "7f8a9b0c-1d2e-3f4a-5b6c-7d8e9f0a1b2c",
  "direction": "outbound",
  "status": "initiated",
  "remote_e164": "+919000000000",
  "bill_duration_seconds": null,
  "billed_duration_seconds": null,
  "duration_seconds": null,
  "cost_credits": null,
  "ai_cost_credits": null,
  "hangup_cause_code": null,
  "hangup_source": null,
  "recording_id": null,
  "metadata": null,
  "created_at": "2026-04-19T12:00:00Z"
}

Status codes

CodeMeaning
200Call created and dialing
400status_callback_url is not a valid public https URL
402Insufficient credits to reserve the call
403Missing telephony:write scope, JWT caller is not an owner/admin, the destination is on your do-not-call list, or the destination country / number range is not allowed
404from_number_id not found/active, or an explicit bot_id not found
422to_e164 is malformed, variables breaks the limits above, or the agent declares a required input variable you did not supply
429Concurrent-call limit reached (up to 10 live calls per tenant), or a burst of calls that looks like toll fraud or robocalling was held

The call row carries two costs. cost_credits is the phone-line leg only: it is filled in after the call by reconciliation (typically within the hour) and stays null for inbound calls. ai_cost_credits is the voice agent (STT, LLM and TTS) for the same call. duration_seconds is the best duration available right now, filled the moment the call ends.

7. List & Fetch Calls

EndpointScopePurpose
GET /callstelephony:readList your calls
GET /calls/{call_id}telephony:readGet one call
GET /calls/{call_id}/recordingtelephony:readSigned recording URL
DELETE /calls/{call_id}telephony:write (owner/admin for JWT)Hang up a live call

GET /calls query parameters

ParamValuesDefault
directioninbound / outbound—
statusinitiated / ringing / in_progress / completed / failed / no_answer / busy—
limit1–20050
offset0–1000000
curl "https://api.callmissed.com/api/v1/telephony/calls?direction=outbound&status=completed&limit=50&offset=0" \
  -H "Authorization: Bearer cm_your_api_key"

Returns an array of calls, newest first. GET /calls/{call_id} fetches a single call (404 if not owned).

Recordings

If a call was recorded, fetch a short-lived signed URL for its audio:

curl https://api.callmissed.com/api/v1/telephony/calls/{call_id}/recording \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK):

{ "url": "https://media.callmissed.com/recordings/....?token=..." }

The URL is time-limited — fetch it on demand rather than storing it. Returns 404 if the call has no recording, and 503 if recording storage is temporarily unavailable.

Hang up a call

curl -X DELETE https://api.callmissed.com/api/v1/telephony/calls/{call_id} \
  -H "Authorization: Bearer cm_your_api_key"

Ends a live call and returns the call, now completed. Idempotent: hanging up a call that has already ended returns it unchanged. 404 if the call is not yours.

8. Click-to-call (bridge a person to a contact)

POST /click-to-call · scope telephony:write (owner/admin for JWT)

Rings your phone first, and once you pick up, dials the contact and puts you both on one line. No AI agent joins. Both legs are outbound calls from your active number and are billed as such; if you do not answer, nothing is charged and the contact is never dialled.

FieldTypeRequiredNotes
destinationstringOne of these twoThe contact's number, in E.164
contact_idUUIDOne of these twoA CRM contact of yours; its phone number is dialled
agent_numberstringNoThe phone to ring first, in E.164. Omit to use the operator phone set in your account settings
bot_idUUIDNoPicks which of your numbers is used as the caller ID, and is recorded on both call rows
curl -X POST https://api.callmissed.com/api/v1/telephony/click-to-call \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"agent_number": "+919000000001", "destination": "+919000000002"}'

Response (200 OK):

{
  "status": "bridged",
  "room_name": "…",
  "agent_call_id": "5e6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
  "destination_call_id": "6f7a8b9c-0d1e-2f3a-4b5c-6d7e8f9a0b1c",
  "destination_e164": "+919000000002",
  "contact_id": null
}

bridged means both legs connected. A leg answered by voicemail also counts as connected, so it does not prove a person is listening. Both call ids work with GET /calls/{call_id} and DELETE /calls/{call_id}.

CodeMeaning
402Insufficient credits to reserve both legs
403The destination is on your do-not-call list, or not an allowed destination
404bot_id or contact_id not found
409No agent_number and no operator phone configured, or no active number to call from
422Neither destination nor contact_id given, or a number is not valid E.164
429Too many calls in progress (a bridge uses two of the 10 concurrent lines)
504Nobody answered your phone, so the contact was not dialled

9. Voicemail drop

POST /calls/{call_id}/voicemail-drop · scope telephony:write (owner/admin for JWT)

Prepares the voicemail message for a call and returns where its audio is. Body: {"template_id": "<uuid>"}, or {} to use the template the call's campaign or agent is configured with. Templates are managed on the Call Handling API.

{
  "action": "voicemail_drop",
  "template_id": "2f8a7c10-5b6d-4e3f-8a1b-7c9d0e1f2a3b",
  "audio_url": "https://…",
  "hangup_after": true
}

A text-only template is turned into speech, cached and billed the first time it is used. audio_url is short-lived. 404 if the call or template is not yours, 409 if no template is configured for the call or it has nothing to say, 503 if speech synthesis or audio storage is unavailable.

Scopes

ScopeGrants
telephony:readSearch numbers, list/get numbers, list/get compliance applications, list/get calls, fetch recording URLs, list BYO providers and connections
telephony:writeBuy/release/update numbers, the AI-call declaration, submit & sync compliance applications, place / bridge / hang up calls, voicemail drops, BYO connections and imports

Every telephony:write action also requires an owner/admin role when called with a JWT (an API key carrying telephony:write is sufficient on its own).

Billing

ChargeWhen
Number rentalMonthly, in credits, per active number (rental_credits). Paid from your real balance — not the signup bonus. Renews on renewal_date.
Call usageReserved when a call is placed, then settled to the real cost from the call record after the call completes.

Releasing a number stops its monthly rental charge (no refund for the current period). Ensure sufficient credits before buying numbers or placing calls, or those calls return 402.

Webhooks

Telephony call-lifecycle and recording-ready events are delivered to the endpoints you configure via the Webhooks API. For one call, status_callback_url on POST /calls delivers the same lifecycle payload to a URL of your choice. Payloads are HMAC-SHA256 signed — verify the X-CallMissed-Signature header exactly as shown on the Webhooks page before trusting a payload.