# Axilio Webhooks

[Axilio](https://axilio.ai) automates real phones: workflow runs, live sessions, files, billing and phone rentals. Its webhooks tell your server when a run finishes, a session ends, a payment fails or a rented phone goes offline, instead of polling. Every delivery is signed with the Standard Webhooks scheme (`webhook-id`, `webhook-timestamp`, `webhook-signature`).

## When to Use This Skill

* How do I receive Axilio webhooks?
* How do I verify an Axilio webhook signature?
* Where do I find my Axilio `whsec_` signing secret?
* How do I handle `run.completed`, `run.failed` or `session.ended` events?
* Why is my Axilio `webhook-signature` verification failing?
* How do I dedupe Axilio webhook retries?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Unique per event, the same as the body's `id`, stable across retries |
| `webhook-timestamp` | Unix seconds of this delivery attempt |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>`, possibly several space-separated entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is the base64-**decoded** part of the destination's secret after `whsec_`. Each destination has its own secret, shown once when you register it. Axilio has no SDK of its own; the examples use `standardwebhooks` (npm, PyPI), which compares in constant time and tries every `v1` entry.

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body string, NOT re-serialised JSON
function verifyAxilioWebhook(rawBody, headers, secret = process.env.AXILIO_WEBHOOK_SECRET) {
  return new Webhook(secret).verify(rawBody, {
    'webhook-id': headers['webhook-id'],
    'webhook-timestamp': headers['webhook-timestamp'],
    'webhook-signature': headers['webhook-signature'],
  }); // returns the parsed event; throws → respond 400
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), {...same three headers...})`. The examples also read the `svix-id`, `svix-timestamp` and `svix-signature` headers when `webhook-id` is absent, as Hookdeck does.

> **Hookdeck retries and the 5-minute window.** The library always rejects a `webhook-timestamp` more than 5 minutes old. Hookdeck retries and replays resend the original timestamp, so the library rejects late retries after 5 minutes. A dependency-free verifier where the window can be dropped or widened is in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/axilio-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/axilio-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/axilio-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/axilio-webhooks/examples/fastapi/).

## Event Envelope

```json
{ "id": "run.completed:0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11", "type": "run.completed",
  "occurred_at": "2026-09-24T12:00:45Z", "test": false,
  "data": { "id": "...", "workflow_id": "...", "status": "completed", "success": true } }

```

`id` is `<type>:<resource id>[:<time>]` and equals `webhook-id`. `test` is `true` only for dashboard tests. `data` uses API field names and carries ids, statuses, reasons and timestamps, never your content (variables, files, recordings, download links). Fetch those through the API.

## Common Event Types

The docs list 28 events (the list may not be exhaustive).

| Area | Events |
| --- | --- |
| Runs | `run.started`, `run.completed`, `run.failed`, `run.cancelled` |
| Sessions | `session.started`, `session.ended`, `session.recording_ready` |
| Billing | `balance.low`, `balance.depleted`, `balance.recovered`, `invoice.paid`, `invoice.payment_failed`, `subscription.updated`, `subscription.canceled`, `auto_recharge.succeeded`, `auto_recharge.failed` |
| Files | `file.ready`, `file.capture_failed`, `file.delivered`, `file.delivery_failed` |
| Phones (rented only) | `phone.maintenance_started`, `phone.maintenance_ended`, `phone.offline`, `phone.online`, `phone.rental.activated`, `phone.rental.past_due`, `phone.rental.canceled` |
| Platform | `webhook.test` |

> **Full payloads:** [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/axilio-webhooks/references/overview.md) and [Axilio's event reference](https://docs.axilio.ai/webhooks/events).

## Delivery Semantics

* **At least once.** Return any `2xx` quickly, then process asynchronously. Any other response, or none, is retried after 30 s, 2 min, 5 min, 15 min and 1 h.
* **Duplicates are possible.** Skip an `id` you have already handled.
* **Order is not guaranteed.** `run.completed` can arrive before `run.started`. Compare `occurred_at` or fetch the resource.
* Failed deliveries can be replayed from the dashboard's delivery log.

## Environment Variables

```bash
AXILIO_WEBHOOK_SECRET=whsec_xxxxx   # the destination's full signing secret; handlers fail closed (500) without it

```

Copy it when you register the destination under **Settings > Webhooks**. It is shown once.

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 axilio --path /webhooks/axilio

```

Register the printed URL under **Settings > Webhooks > Register Destination**, then use **Test** on the destination to send an example event.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/axilio-webhooks/references/overview.md) - Event catalog, payload fields, duplicate ids, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/axilio-webhooks/references/setup.md) - Registering a destination, signing secret, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/axilio-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging