# Webhooks

Get real-time updates instead of polling. Each event is POSTed as JSON to your HTTPS URL.

## Setup

```bash
curl -X POST https://smsprovider-web.vercel.app/api/v1/webhooks \
  -H "Authorization: Bearer $BATLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/batler", "events": ["message.delivered", "message.failed", "inbound.received"]}'
```

The response includes a `secret` (`whsec_…`) — **shown only once**. Store it to verify signatures.

## Events

| Event | When |
| --- | --- |
| `message.sent` | Handed to the mobile network |
| `message.delivered` | Delivery receipt from the handset |
| `message.failed` | Failure, with `reason` (e.g. `blocked`, `no_coverage`, `kosher_phone`) |
| `message.expired` | No delivery receipt in time |
| `inbound.received` | A reply to your sender number |
| `otp.verified` / `otp.failed` | OTP verification outcome |
| `wallet.low_balance` / `wallet.credited` | Low balance / credits purchased |
| `blacklist.added` | A recipient opted out |
| `link.clicked` | A tracked link was clicked |

Shape:

```json
{
  "id": "4f0c7c1e-1c2b-4a0e-9d7b-2e3f4a5b6c7d",
  "type": "message.delivered",
  "created_at": "2026-11-02T18:00:05Z",
  "data": { "message_id": "0e9b2f0a-6c1d-4f43-8c33-6f3b3f0d9a11", "to": "+972501234567", "ref": "order-123", "status": "delivered", "reason": null, "operator": "Partner", "occurred_at": "2026-11-02T18:00:04Z" }
}
```

## Verifying the signature

Every request carries `X-Batler-Signature: t=<unix seconds>,v1=<hex>` where `v1 = HMAC-SHA256(secret, "<t>.<raw body>")`, plus `X-Batler-Event-Id` for deduplication. Compute the signature over the **raw body** and reject timestamps older than 5 minutes.

### Node.js

```ts
import { verifyWebhookSignature } from "@batler/sms";

app.post("/hooks/batler", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!(await verifyWebhookSignature(process.env.BATLER_WEBHOOK_SECRET!, req.get("X-Batler-Signature"), raw))) return res.sendStatus(401);
  const event = JSON.parse(raw);
  // dedupe by req.get("X-Batler-Event-Id"), then handle event.type
  res.sendStatus(200);
});
```

### PHP

```php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_BATLER_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $p);
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, getenv('BATLER_WEBHOOK_SECRET'));
if (!isset($p['v1']) || !hash_equals($expected, $p['v1']) || abs(time() - (int)$p['t']) > 300) {
    http_response_code(401); exit;
}
$event = json_decode($raw, true);
http_response_code(200);
```

### Python

```python
import hmac, hashlib, time

def verify(secret: str, header: str, raw: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
```

## Retries

Return 2xx within 10 seconds. Otherwise we retry after 1m, 5m, 30m, 2h, 6h and 24h. After 10 consecutive failures the endpoint is disabled and you get an email. Delivery log and replay: `GET https://smsprovider-web.vercel.app/api/v1/webhooks/{id}/deliveries` and `…/replay`.
