API reference

Webhooks

Receive real-time HTTP notifications when calls and messages change state, with HMAC-signed payloads and durable delivery.

A webhook endpoint is a URL on your server that JagCall sends event notifications to. When something happens — a call completes, an SMS arrives — we POST a signed JSON payload to every endpoint subscribed to that event. Delivery is durable: failed attempts are retried with exponential backoff.

Creating an endpoint

Add endpoints from the dashboard under Tools → Webhooks, or via the API. Each organization can have up to 10 endpoints. Endpoint URLs must be publicly reachable — https:// is strongly recommended, and private, loopback, and reserved IPs are rejected. Omit events to subscribe to every event, or pass an array to filter.

POST/v1/webhook-endpoints

Create an endpoint. The response includes the signing secret exactly once — store it securely.

Request body

json
{ "url": "https://your-app.com/webhooks/jagcall", "events": ["call.completed", "sms.received"] }

Response

json
{ "id": "3f2a1c9e-8b4d-4e21-9f77-0a1b2c3d4e5f", "url": "https://your-app.com/webhooks/jagcall", "events": ["call.completed", "sms.received"], "enabled": true, "has_secret": true, "secret": "whsec_...", "created_at": "2025-11-01T00:00:00Z" }

Example

curl
curl -X POST https://jagcall.com/v1/webhook-endpoints \  -H "Authorization: Bearer jc_live_..." \  -H "Content-Type: application/json" \  -d '{ "url": "https://your-app.com/webhooks/jagcall", "events": ["call.completed", "sms.received"] }'

One-time secret

The signing secret (whsec_…) is returned only when you create the endpoint or rotate its secret. Reveal it later from the dashboard, or rotate to generate a new one.

Event types

Subscribe to any of these events:

EventFires when
call.startedA call has connected and started.
call.ringingAn outbound call is ringing the destination.
call.completedA call ended normally.
call.failedA call failed to connect or errored.
call.takeoverA human agent took over a live call.
call.transcript.readyThe transcript for a completed call is available.
sms.receivedAn inbound SMS was received.
sms.sentAn outbound SMS was sent.
simulation.completedAn agent test simulation finished.

Payload format

Every delivery is a POST with a JSON body shaped as { event, data, timestamp }. The timestamp is an ISO-8601 UTC time; data depends on the event. All six call.* events share the same call shape:

json
{  "event": "call.completed",  "data": {    "call_id": "call_a1b2c3",    "agent_id": "agt_x1y2z3",    "direction": "outbound",    "status": "completed",    "from_number": "+14155551234",    "to_number": "+14155559876",    "duration_seconds": 142,    "cost_total": 0.0231,    "error_message": null,    "started_at": "2025-11-01T14:30:00+00:00",    "ended_at": "2025-11-01T14:32:22+00:00",    "created_at": "2025-11-01T14:29:58+00:00"  },  "timestamp": "2025-11-01T14:32:22.123456+00:00"}

SMS events use a message shape:

json
{  "event": "sms.received",  "data": {    "message_id": "sms_a1b2c3",    "conversation_id": "conv_x1y2z3",    "direction": "inbound",    "from_number": "+14155559876",    "to_number": "+14155551234",    "body": "Yes, tomorrow at 2pm works!",    "status": "received",    "contact_name": "Jane Doe",    "created_at": "2025-11-01T14:35:00+00:00"  },  "timestamp": "2025-11-01T14:35:00.500000+00:00"}

Delivery id is in the headers, not the body

The unique delivery identifier is sent as the X-Webhook-Delivery-Id header (see below), not inside the JSON body.

Request headers

HeaderDescription
Content-Typeapplication/json
X-Webhook-EventThe event type, e.g. call.completed.
X-Webhook-TimestampUnix timestamp (seconds) when the request was signed.
X-Webhook-Signaturet=<ts>,v1=<hmac> — one or more v1 values (see rotation).
X-Webhook-Delivery-IdStable UUID for this delivery. Identical across retries — deduplicate on it.

Signature verification

Each signed request carries an X-Webhook-Signature header of the form t=<timestamp>,v1=<signature>. The signature is the hex HMAC-SHA256 of the string <timestamp>.<raw request body>, keyed with your endpoint's signing secret (the full whsec_… value). Verify against the raw request body bytes exactly as received — do not re-serialize the JSON.

python
import hashlibimport hmacimport timedef verify(secret: str, headers: dict, raw_body: bytes) -> bool:    sig_header = headers["X-Webhook-Signature"]   # "t=1730470000,v1=abc...,v1=def..."    items = [p.split("=", 1) for p in sig_header.split(",")]    timestamp = next(v for k, v in items if k == "t")    # Reject stale timestamps to prevent replay (5 minute tolerance).    if abs(time.time() - int(timestamp)) > 300:        return False    signed = f"{timestamp}.".encode() + raw_body          # verify against the RAW body bytes    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()    # A delivery may carry multiple v1 signatures during a secret rotation.    provided = [v for k, v in items if k == "v1"]    return any(hmac.compare_digest(expected, v) for v in provided)
node.js
const crypto = require('crypto')function verify(secret, headers, rawBody /* Buffer */) {  const sigHeader = headers['x-webhook-signature']   // "t=...,v1=...,v1=..."  const items = sigHeader.split(',').map((p) => p.split('='))  const timestamp = items.find(([k]) => k === 't')?.[1]  // Reject stale timestamps to prevent replay (5 minute tolerance).  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false  const expected = crypto    .createHmac('sha256', secret)    .update(timestamp + '.')    .update(rawBody)                                 // verify against the RAW body bytes    .digest('hex')  // A delivery may carry multiple v1 signatures during a secret rotation.  const provided = items.filter(([k]) => k === 'v1').map(([, v]) => v)  return provided.some(    (v) => v.length === expected.length &&      crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected))  )}
php
<?phpfunction verify(string $secret, array $headers, string $rawBody): bool {    $sigHeader = $headers['X-Webhook-Signature'];   // "t=...,v1=...,v1=..."    $parts = [];    foreach (explode(',', $sigHeader) as $piece) {        [$k, $v] = explode('=', $piece, 2);        $parts[$k][] = $v;    }    $timestamp = $parts['t'][0] ?? '';    // Reject stale timestamps to prevent replay (5 minute tolerance).    if (abs(time() - (int) $timestamp) > 300) return false;    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);    // A delivery may carry multiple v1 signatures during a secret rotation.    foreach ($parts['v1'] ?? [] as $sig) {        if (hash_equals($expected, $sig)) return true;    }    return false;}

Verify safely

Always (1) compare signatures in constant time, (2) reject requests whose X-Webhook-Timestamp is more than a few minutes old to prevent replay attacks, and (3) accept the request if any v1 value matches — a delivery is dual-signed during a secret rotation.

Retries & delivery

Delivery is at-least-once. Your endpoint should return a 2xx status within 10 seconds. Any non-2xx response, timeout, or network error is retried with exponential backoff — up to 6 attempts total:

AttemptSent
1Immediately
230 seconds after attempt 1 fails
32 minutes later
410 minutes later
51 hour later
64 hours later

After the 6th attempt fails, the delivery is marked failed. Responding 410 Gone stops retries for that delivery immediately. An endpoint that accumulates 20 consecutive failures is automatically disabled; any successful delivery resets the counter.

Handle duplicates

Because delivery is at-least-once, the same event can arrive more than once (for example, if your server responds slowly and we retry). Make your handler idempotent by deduplicating on the X-Webhook-Delivery-Id header, which is stable across all retries of a delivery.

Secret rotation

Rotating an endpoint's secret generates a new whsec_… value and returns it once. For a 24-hour grace window, deliveries are signed with both the new and previous secrets (two v1= values in the signature header), so you can roll the stored secret on your side at any point without missing a verification. After the window, only the new secret signs.

POST/v1/webhook-endpoints/{endpoint_id}/rotate-secret

Rotate the signing secret. Returns the new secret and the previous secret's expiry.

Response

json
{ "secret": "whsec_...", "previous_secret_expires_at": "2025-11-02T00:00:00Z" }

Example

curl
curl -X POST "https://jagcall.com/v1/webhook-endpoints/3f2a1c9e-8b4d-4e21-9f77-0a1b2c3d4e5f/rotate-secret?grace=true" \  -H "Authorization: Bearer jc_live_..."

Replaying deliveries

Inspect and replay past deliveries from the dashboard, or via the API. A replay creates a new delivery (with its own X-Webhook-Delivery-Id) that references the original.

POST/v1/webhook-deliveries/{delivery_id}/replay

Re-enqueue a past delivery to the same endpoint.

Example

curl
curl -X POST https://jagcall.com/v1/webhook-deliveries/9b8c7d6e-5a4b-3c2d-1e0f-a1b2c3d4e5f6/replay \  -H "Authorization: Bearer jc_live_..."

Testing

Use the Test button on any endpoint in the dashboard to send a sample call.completed event and see the response. For local development, expose your server with a tunnel such as ngrok and point an endpoint at the public URL. See the full API reference for all webhook management endpoints.

Still need help?

Can't find what you're looking for? Send us a message and our team will get back to you.

Contact support