Skip to main content

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

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_number and customer_number must differ (400 SAME_NUMBER) — there’d be nobody to bridge.
  • did must be a number your workspace owns, on the trunk you’re calling over (403 CALLER_ID_NOT_OWNED).
  • There is no allowlist of agent numbers. Any number passing the rules above can be the agent.

Call lifecycle

The state 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:
A reference is permanent, and a failed call still consumes it. To try again after a call that rang out or failed, place a new call with a new reference_id. Re-sending the old one replays a dead record and dials nothing — this is the most common cause of “the API said success but the phone never rang”.The exception is a refusal that provably placed no call — rate limiting, no free channel, carrier down, or a misconfigured server. Those leave the reference unconsumed, so retry with the same one.
Allowed characters: letters, digits, ., _, :, -; 1–128 characters (400 INVALID_REFERENCE otherwise).

Webhooks

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

Two details decide whether this works: use the secret as you sent it (no decoding step), and use the raw body bytes. Re-serialising parsed JSON changes whitespace and key order and breaks the signature — capture the raw body before your JSON middleware touches it.
Reject anything that fails verification with a 4xx. Never process an unverified payload.

Events

A call emits call.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

The 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.
duration and answer_duration are strings, and their meanings differ by surface. On a webhook, duration is seconds since the call was admitted and answer_duration is talk time. The REST call object avoids the ambiguity with total_seconds and talk_seconds — both integers.

Receiver requirements

  • Respond 2xx within 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 on 408, 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

Pass record: 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 on code, 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_secret per integration, generated randomly and stored alongside the key.
Placing calls
  • Store the returned call_id against your own record.
  • Treat 201 and 200 as success, and read placed / replayed — 200 means nothing new was dialled.
  • After a failed call, place a new one with a new reference_id.
  • Back off on 429/503 using Retry-After, retrying the same reference_id.
Receiving webhooks
  • Capture the raw body before JSON parsing.
  • Verify x-qcall-signature in constant time; reject failures with 4xx.
  • Check x-qcall-timestamp freshness.
  • Deduplicate on x-qcall-event-id.
  • Return 2xx within 5 s and process asynchronously.
  • Handle all five events; expect exactly one terminal event per call.
  • Remember the webhook’s duration / answer_duration are strings.
Resilience
  • Reconcile with ?refresh=1 for any call with no terminal event after its maximum life.
  • Use GET /dialer/click2call/{id}/events when a delivery is in doubt.
  • Alert on repeated 503 and on 429 bursts.

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