# Call Completed Webhook

> The call_tracking.call.completed webhook - one signed POST per call on a tracking number with caller, status, duration, price, match type and the visit's UTM tags and click IDs. Subscribe, verify and retry.

Source: https://edesy.in/docs/call-tracking/data/webhook

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](https://voice-app.edesy.in) → **Webhooks**) or through the API:

```bash
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

```json
{
  "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](https://edesy.in/docs/call-tracking/pools/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.

```js
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](https://edesy.in/tools/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.
