# Webhooks

> The call.completed and call.attempt_failed events, signature verification and retries.


# Webhooks

Allerto posts JSON events to the URL in the agent's `delivery` (or to a call's `callback_url`).

| Event                 | When                                                                                |
| --------------------- | ----------------------------------------------------------------------------------- |
| `call.completed`      | Once per call, when it has ended for good: with outcome, result, summary, sentiment |
| `call.attempt_failed` | An outbound attempt was not answered and another is scheduled (`next_attempt_at`)   |

```json
{
  "id": "evt-uuid",
  "type": "call.completed",
  "version": "2",
  "created_at": "2026-09-21T14:29:14Z",
  "livemode": true,
  "partner_id": "…",
  "call": { "…": "the call object, see Calls and campaigns" }
}
```

`livemode: false` marks events produced by test keys. For inbound agents with `review: on_issues`, the event is sent after an operator approves the data when something was missing.

## Signing secret

One secret per account signs every event:

```http
POST /v1/webhook-secret        →  {"secret": "whsec_…"}
```

```sh
allerto webhook-secret rotate
```

It is shown once. Rotating affects new events; events already queued keep the previous secret. An agent whose delivery uses a webhook cannot be published until a secret exists.

## Verify every request

Headers:

```http
Content-Type: application/json
Idempotency-Key: <event id>
X-Allerto-Event-Id: <event id>
X-Allerto-Timestamp: <unix seconds>
X-Allerto-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
```

Node.js:

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, headers, secret) {
  const ts = headers["x-allerto-timestamp"];
  const got = Buffer.from(
    String(headers["x-allerto-signature"]).replace(/^sha256=/, ""),
    "hex",
  );
  const want = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest();
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && got.length === want.length && timingSafeEqual(got, want);
}
```

Python:

```python
import hmac, hashlib, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers["X-Allerto-Timestamp"]
    got = headers["X-Allerto-Signature"].removeprefix("sha256=")
    want = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(ts)) < 300 and hmac.compare_digest(got, want)
```

Always verify against the **raw** body, before parsing JSON.

## Delivery guarantees

- At least once: store the event id and ignore repeats. The id and payload never change between retries; timestamp and signature do.
- Any `2xx` acknowledges. Redirects are not followed. Timeout: 10 seconds.
- Up to 5 attempts with growing delays (15 s, 30 s, 60 s, …, max 1 h).
- Destinations must be HTTPS on port 443 and resolve to public addresses.
- `GET /v1/events` (or `allerto events list`) shows recent events and their delivery status.

## Email delivery

Inbound agents can also send each result by email (`delivery.mode: "email"` or `"both"`), with the subject and introduction you configure and, optionally, a short AI summary. Test keys never send emails.
