callback_url and callback_secret to Place Click-to-Call and every event for that call is posted to your endpoint, signed with the secret you chose.
The webhook URL is per call, not per account. Nothing is registered in advance, so a new environment, a new tenant, or a tunnel URL that changes every morning all work with no configuration change on our side. The only thing you reuse is the secret.
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.
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".Headers
Payload
string
One of the five event names above
string
Matches
x-qcall-event-idinteger
1 for the first event of a call, 2 for the secondinteger
Status code for the event, per the table above
string
Human-readable summary, e.g.
Call completed.string | null
Mirrors
data.error; null on successobject
object
Added by 60db, naming the call in your terms — match on these rather than anything else in the envelope
Verifying the signature
- Use the secret exactly as you sent it. There is no decoding step — the HMAC key is the string’s own UTF-8 bytes.
- 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.
- Node.js (Express)
- Python (Flask)
4xx. Never process an unverified payload — the URL is public, and without the signature anyone who learns it could post a call.completed your system then acts on.
Receiver requirements
- Respond
2xxwithin 5 seconds. Acknowledge first, process asynchronously — a slow handler is a failed delivery. - Deduplicate on
x-qcall-event-id; the same event can arrive more than once. - Make processing idempotent.
- Use a constant-time comparison (
crypto.timingSafeEqual,hmac.compare_digest). - Check
x-qcall-timestampagainst your clock with a tolerance of a few minutes. - 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 addresses that passed — 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, any5xx, and network or timeout errors. - Not retried on other
4xx— a401from a broken signature check is a permanent rejection.
When a delivery is in doubt
GET /dialer/click2call/{id}/events shows every event for a call, how many attempts were made, the status your endpoint returned and when it was delivered. GET /dialer/click2call/{id}?refresh=1 is the state backstop — poll a call you have not heard about rather than polling every call on a timer.