> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracklysms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Queue Webhooks

> Receive call.received and call.ended events for inbound calls routed to a queue, optionally signed.

Attach a `notifyUrl` to a [queue](/api-reference/voice/queues/create-queue) to be notified when an inbound call arrives at the queue and when it ends. Trackly `POST`s a small JSON payload to your URL; if you also set a `notifySecret`, each request is signed so you can verify it came from Trackly.

## Configuring

Set `notifyUrl` (and optionally `notifySecret`) when you [create a queue](/api-reference/voice/queues/create-queue):

```bash theme={null}
curl -X POST https://api.tracklysms.com/api/v1/voice/queues \
  -H "X-Api-Key: trk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support",
    "holdAudioUrl": "https://cdn.example.com/hold.mp3",
    "notifyUrl": "https://example.com/hooks/voice",
    "notifySecret": "whsec_your_shared_secret"
  }'
```

The `notifyUrl` must be a public HTTPS URL.

## Events

| Event           | When it fires                                                     |
| --------------- | ----------------------------------------------------------------- |
| `call.received` | An inbound call is routed to this queue.                          |
| `call.ended`    | An inbound call belonging to this queue reaches a terminal state. |

## Payload

The request body is a JSON object. Fields are serialized with sorted keys, so verify the signature against the exact raw bytes you receive.

```json theme={null}
{
  "event": "call.received",
  "callId": "665f1e2a9c4b1a0012ab34cd",
  "queueId": "665f1e2a9c4b1a0012ab90ef",
  "from": "+14155551234",
  "to": "+18005551234",
  "state": "ringing"
}
```

<ResponseField name="event" type="string">`call.received` or `call.ended`.</ResponseField>
<ResponseField name="callId" type="string">The call's id — look it up with [Get a Call](/api-reference/voice/calls/get-call).</ResponseField>
<ResponseField name="queueId" type="string">The queue that emitted the event.</ResponseField>
<ResponseField name="from" type="string">The caller's number (E.164).</ResponseField>
<ResponseField name="to" type="string">The number that was dialed (E.164).</ResponseField>
<ResponseField name="state" type="string">The call's [state](/api-reference/voice/overview#call-states) at the time of the event.</ResponseField>

## Verifying the signature

When a `notifySecret` is configured, Trackly sends an `X-Trackly-Signature` header containing the hex-encoded HMAC-SHA256 of the **raw request body**, keyed by your secret. Recompute it and compare in constant time.

```python theme={null}
import hmac, hashlib

def is_valid(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")
```

If no `notifySecret` is set, the `X-Trackly-Signature` header is omitted.

## Delivery semantics

<Warning>
  Queue notifications are **best-effort**: a single `POST` with a 5-second timeout, no retries and no delivery ledger. Respond quickly with a `2xx` and do any slow work asynchronously. Treat the webhook as a low-latency hint and reconcile authoritative state with [List Calls](/api-reference/voice/calls/list-calls) / [Get a Call](/api-reference/voice/calls/get-call).

  The signature covers the request body only — it does not include a timestamp — so it does not by itself defend against a replayed capture. Treat events as idempotent (dedupe on `callId` + `event`) and use the API as the source of truth rather than acting solely on a received event.
</Warning>
