Edesy sends call_tracking.call.completed once for every call on a tracking number, after the call ends. Use it to put each call and its campaign into your CRM, database or reporting.
Subscribe#
Add a webhook subscription in the voice app (voice-app.edesy.in → Webhooks) or through the API:
curl -X POST https://voice-api.edesy.in/v1/webhooks/subscriptions \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/edesy",
"event_types": ["call_tracking.call.completed"],
"secret": "a-long-random-string"
}'
Leave event_types empty to receive every voice event. Send a test with POST/v1/webhooks/subscriptions/{id}/test.
Request#
POST to your URL with Content-Type: application/json and these headers:
| Header | Value |
|---|---|
X-Edesy-Event |
call_tracking.call.completed |
X-Edesy-Signature |
sha256=<hex>: HMAC-SHA256 of the raw body with your secret (when a secret is set) |
Payload#
{
"event": "call_tracking.call.completed",
"timestamp": "2026-11-14T06:12:44Z",
"data": {
"call_sid": "CA_...",
"started_at": "2026-11-14T06:10:02Z",
"caller": "+9198XXXXXXXX",
"tracking_number": "+9122XXXXXXXX",
"status": "completed",
"duration_sec": 154,
"price": 3,
"connected_number": "+9190XXXXXXXX",
"pool_id": "4b1d...",
"match": "exact",
"is_first_call": true,
"recording_available": false,
"attribution": {
"match": "exact",
"session_id": "9e2c...",
"site_host": "www.example.com",
"landing_url": "https://www.example.com/flats?utm_source=google&utm_medium=cpc&utm_campaign=2bhk&gclid=Cj0KCQiA...",
"referrer": "https://www.google.com/",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "2bhk",
"utm_term": "2bhk flats",
"gclid": "Cj0KCQiA...",
"first_touch": {
"utm_source": "facebook",
"utm_medium": "paid_social",
"fbclid": "IwAR...",
"landing_url": "https://www.example.com/",
"at": "2026-11-02T14:03:11Z"
}
}
}
}
| Field | Meaning |
|---|---|
match |
exact, grace, repeat_caller or none. See Attribution Rules |
is_first_call |
The caller had never called this pool before |
price |
What the call cost from your wallet, in rupees |
connected_number |
The phone that answered, when the call connected |
attribution |
The credited visit; only match is set when the call is unattributed. Empty fields are left out |
recording_available |
Whether the call has a recording |
Verify the signature#
Compute HMAC-SHA256 over the raw request body with your secret and compare it to X-Edesy-Signature in constant time.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return header && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
You can check a signature by hand with the webhook signature verifier.
Retries#
Respond with any 2xx within 5 seconds. Any other response, or a timeout, is retried with backoff starting at 30 seconds and capped at one hour, up to 6 attempts in total. Failed deliveries can be replayed from the subscription's delivery log. Use call_sid to ignore duplicates.
FAQ#
Is the event sent for unanswered calls?
Yes. It is sent for every call on a tracking number; check status and duration_sec.
Is the event sent when the call was unattributed?
Yes, with attribution.match set to none.
Can I get the event in Call Events v1 format?
call_tracking.call.completed is delivered to standard (legacy-format) subscriptions with the X-Edesy-Signature header.