> For the complete documentation index, see [llms.txt](https://docs.limephone.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.limephone.io/api-reference/webhooks.md).

# Webhooks

Subscribe an HTTPS endpoint to events and LimePhone will POST each occurrence to you as it happens — no polling.

## Events

| Event              | Fires when                  |
| ------------------ | --------------------------- |
| `call.created`     | A call starts               |
| `call.completed`   | A call ends                 |
| `call.failed`      | A call fails                |
| `message.sent`     | An outbound message is sent |
| `message.received` | An inbound message arrives  |
| `message.failed`   | A message fails to deliver  |

## Delivery

Deliveries are JSON POSTs with a 10-second timeout. A non-2xx response is retried with backoff up to your subscription's `maxRetries` (default 3). Each delivery carries:

| Header                  | Meaning                                                       |
| ----------------------- | ------------------------------------------------------------- |
| `X-Webhook-Event`       | The event name, e.g. `call.completed`                         |
| `X-Webhook-Delivery-Id` | Unique id for this delivery (stable across retries)           |
| `X-Webhook-Signature`   | HMAC-SHA256 signature — verify it before trusting the payload |

## Verifying signatures

Every subscription has a `secret`, returned when you create it. The signature is `HMAC-SHA256(secret)` over the string `{timestamp}.{deliveryId}.{rawBody}`, where `timestamp` is the `timestamp` field inside the payload and `rawBody` is the exact JSON body as received. Compare with a constant-time comparison and reject anything that doesn't match.

```javascript
// Node.js — capture the raw request body for signing
const crypto = require('crypto');

function verify(req, secret) {
  const payload = JSON.parse(req.rawBody);
  const canonical = payload.timestamp + '.' +
    req.headers['x-webhook-delivery-id'] + '.' + req.rawBody;
  const expected = crypto.createHmac('sha256', secret)
    .update(canonical).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(req.headers['x-webhook-signature']));
}
```

```python
# Python — same verification with Flask
import hashlib, hmac, json

def verify(raw_body: bytes, headers, secret: str) -> bool:
    payload = json.loads(raw_body)
    canonical = f"{payload['timestamp']}.{headers['X-Webhook-Delivery-Id']}.{raw_body.decode()}"
    expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers.get("X-Webhook-Signature", ""))
```

## A complete receiver

```javascript
// Node.js / Express — capture the RAW body (signature covers the exact bytes)
const express = require('express');
const crypto = require('crypto');

const app = express();
app.post('/hooks/limephone',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const raw = req.body.toString('utf8');
    const payload = JSON.parse(raw);
    const canonical = `${payload.timestamp}.${req.headers['x-webhook-delivery-id']}.${raw}`;
    const expected = crypto.createHmac('sha256', process.env.LIMEPHONE_WEBHOOK_SECRET)
      .update(canonical).digest('hex');
    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-webhook-signature'] || ''))) {
      return res.status(401).end();
    }
    switch (req.headers['x-webhook-event']) {
      case 'call.completed':
        console.log('Call finished:', payload.data);
        break;
      case 'message.received':
        console.log('Inbound message:', payload.data);
        break;
    }
    res.status(200).end(); // respond fast; do heavy work async
  });
app.listen(3000);
```

## Example event payload

```json
{
  "event": "call.completed",
  "timestamp": "2026-09-04T14:03:22.000Z",
  "data": {
    "id": "call_abc123",
    "direction": "inbound",
    "from": "+14155550134",
    "to": "+442080580300",
    "status": "completed",
    "duration": 184
  }
}
```

## List webhooks

`GET /v1/webhooks` — scope `webhooks:read`

Lists your webhook subscriptions. Also available: `GET /v1/webhooks/{id}` for one.

## Create a webhook

`POST /v1/webhooks` — scope `webhooks:write`

Creates a subscription. The response includes the signing `secret` — store it now; treat it like a password.

| Field         | Type         | Required | Notes                                       |
| ------------- | ------------ | -------- | ------------------------------------------- |
| `url`         | string       | yes      | HTTPS endpoint to receive deliveries        |
| `events`      | string\[]    | yes      | One or more event names from the list above |
| `description` | string       | no       |                                             |
| `maxRetries`  | integer 0–10 | no       | Retry attempts on failure. Default 3        |

```bash
curl -X POST https://dashboard.limephone.io/api/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/limephone",
    "events": ["call.completed", "message.received"]
  }'
```

## Update a webhook

`PATCH /v1/webhooks/{id}` — scope `webhooks:write`

Change the URL or events, pause with `isActive: false`, or adjust `maxRetries`.

## Delete a webhook

`DELETE /v1/webhooks/{id}` — scope `webhooks:write`

Deletes a subscription. Returns `204`. Deliveries stop immediately.
