Overview
Click-to-Call places a two-leg phone call from a single authenticated HTTPS request. It rings your agent first on your own rented number, and only once the agent answers does it ring the customer — then it bridges the two. Both legs present your DID as the caller ID. There is no WebRTC, no SIP registration and no microphone involved, which is the point: it works from a phone, from a laptop with no headset, and for an agent who isn’t logged into your app at all.This is not the same endpoint as Reserve Call.
POST /dialer/calls reserves an id for a call your own SIP client places. Click-to-Call dials both phones itself. They share your trunk, your numbers, your wallet and the carrier-health gate — nothing else.One request, two phones
No softphone or SIP stack on your side
Agent-first dialling
Your agent is committed before the customer’s phone rings
Signed webhooks
Every call event relayed to your URL, HMAC-signed with your own secret
Per-second billing
₹0.30/min ($0.003/min) on talk time only — unanswered calls are free
Prerequisites
1
Trunk setup (Gate T)
Dashboard → Dialer → Trunks. Without a provisioned trunk every click-to-call endpoint returns 409
DIALER_NOT_PROVISIONED.2
At least one rented number
The
did you dial out on must be assigned to your workspace and to the trunk you’re calling over. Buy one via POST /dialer/pool/allocate (requires KYC approval, Gate K).3
A positive wallet balance
A balance of
0 or less refuses the next call with 402 RECHARGE_REQUIRED. A call already in progress always finishes and is always billed.4
A public HTTPS webhook (optional, recommended)
Pass
callback_url + callback_secret on each call to receive its events. Without them, poll GET /dialer/click2call/{id} instead.Quickstart
- cURL
- Node.js
- Python
outbound_trunk_id and did are optional: they default to your workspace’s active trunk and its saved caller ID, so the common case is a two-field request.
Phone numbers
agent_number, customer_number and did accept Indian numbers in any of these forms. Spaces, hyphens, dots and round brackets are stripped before validation.
The number must normalise to
+91 followed by ten digits whose first digit is 2–9. Everything is stored and returned as +E.164 — send 9812345678, read back +919812345678.
agent_numberandcustomer_numbermust differ (400SAME_NUMBER) — there’d be nobody to bridge.didmust be a number your workspace owns, on the trunk you’re calling over (403CALLER_ID_NOT_OWNED).- There is no allowlist of agent numbers. Any number passing the rules above can be the agent.
Call lifecycle
Thestate field on a call is the coarse state your UI renders:
When a call settles,
terminal_event names how, and hangup_cause and leg say why and on which side:
A decline and an unanswered call are not reliably distinguishable on this carrier route, so both usually report as
call.not_answered with hangup_cause: "NO_ANSWER".Idempotency: reference_id
reference_id is the idempotency key, unique per trunk. Omit it and one is minted for you (qc2c-<uuid>), which is the right default unless you’re retrying.
Supply your own (a CRM task id, a ticket id) when you need a request to be safely repeatable:
Allowed characters: letters, digits,
., _, :, -; 1–128 characters (400 INVALID_REFERENCE otherwise).
Webhooks
Passcallback_url and callback_secret on a call and every event for that call is forwarded to your endpoint, signed with the secret you chose.
The secret is per call and yours to pick — 16 to 256 characters of anything random. It is required whenever you send a
callback_url: an unsigned webhook is one your receiver cannot tell from a forgery. It is encrypted at rest and never shown again; callback_secret_last4 on the call tells you which one we hold.Headers on every delivery
The signature
- Node.js (Express)
- Python (Flask)
4xx. Never process an unverified payload.
Events
A call emitscall.answered only if the two parties were bridged, and exactly one terminal event, always. A call nobody answered produces one event; a call that connects produces two.
Payload
qcall block is added by 60db and names the call in your terms — the ids you got back from POST /dialer/click2call. Match on those rather than on anything else in the envelope.
Receiver requirements
- Respond
2xxwithin 5 seconds — acknowledge first, process asynchronously. - Deduplicate on
x-qcall-event-id; the same event can arrive more than once. - Make processing idempotent.
- Serve HTTPS on a publicly resolvable host. Private, loopback, link-local and CGNAT addresses are rejected, and the destination is re-validated (with the socket pinned to the validated addresses) before every delivery — so DNS rebinding can’t redirect a delivery after the fact.
Retries
Up to 6 attempts per event, at least two minutes apart, abandoned once the event is 24 hours old. Retried on408, 425, 429, any 5xx, and network or timeout errors. Any other 4xx is treated as a permanent rejection and not retried.
Design for at-least-once delivery, out-of-order arrival, and the possibility that an event never arrives at all — then use GET /dialer/click2call/{id}/events to see exactly what was sent and what your endpoint replied, and ?refresh=1 on a single call as the backstop.
Recording
Passrecord: true to ask for audio. Recording is produced by the call platform and uploaded after the call ends, so it appears on the call a little later than the terminal event.
record_requested and recording_available are two separate facts on purpose: a call that asked for audio and has none is a different thing from a call that never asked. The playback URL expires in minutes and is never stored — mint a new one when you need it.
Reconciliation
Webhooks are the primary mechanism; polling is the fallback. Poll a call you haven’t heard about, rather than polling every call on a timer.GET /dialer/click2call/{id} reads the stored call cheaply. Add ?refresh=1 and it asks the platform for the authoritative state first — which is what repairs a call whose terminal webhook never arrived, and what picks up a recording that finished uploading.
The {id} accepts either the call_id or your reference_id, so you can reconcile straight from your own records without storing ours.
Behind the scenes a reconciler runs every few minutes to replay unapplied events, resolve placements whose outcome was never confirmed, poll calls that have outlived all their timeouts, and retry undelivered relays. A call left in unknown normally resolves itself within that window.
Pricing
Each call carries its own cost on the call object, so you never have to reconcile against a separate ledger by timestamp:
A call that starts always finishes and is always billed, even if it takes the wallet negative. The balance check gates the next call, not the one in flight.
Limits
The per-minute limit is keyed to the caller, not the IP — a shared office NAT won’t throttle a whole floor.
Error codes
Every failure returns the same envelope. Branch oncode, never on message.
request_id is present on refusals that came from the call platform — log it, it is the handle 60db support traces a request on. retry_after accompanies 429 and 503 and mirrors the Retry-After header, in seconds.
Your request
Your account
Upstream and server-side
429 and 503 carry a Retry-After header in seconds. A refused request never places a call and never consumes its reference_id, so those are safe to retry with the same one.Integration checklist
Setup- Trunk provisioned, at least one number rented, wallet funded.
- API key in a secret manager. Server-side only — never in a browser or mobile app.
- A
callback_secretper integration, generated randomly and stored alongside the key.
- Store the returned
call_idagainst your own record. - Treat 201 and 200 as success, and read
placed/replayed—200means nothing new was dialled. - After a failed call, place a new one with a new
reference_id. - Back off on
429/503usingRetry-After, retrying the samereference_id.
- Capture the raw body before JSON parsing.
- Verify
x-qcall-signaturein constant time; reject failures with4xx. - Check
x-qcall-timestampfreshness. - Deduplicate on
x-qcall-event-id. - Return
2xxwithin 5 s and process asynchronously. - Handle all five events; expect exactly one terminal event per call.
- Remember the webhook’s
duration/answer_durationare strings.
- Reconcile with
?refresh=1for any call with no terminal event after its maximum life. - Use
GET /dialer/click2call/{id}/eventswhen a delivery is in doubt. - Alert on repeated
503and on429bursts.
API Reference
Place Call
Ring the agent, then the customer
Get Call
State, outcome and cost
List Calls
Click-to-call history
List Events
Events and delivery attempts
Recording URL
Short-lived playback link
Get Config
Readiness and limits
Webhook Events
Payloads, headers, signatures