> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axilio.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook event reference

> Every webhook event Axilio sends: when it fires, what its data contains, and how to recognise a duplicate.

Each event describes one change to one resource. Its `type` names the resource
and what happened to it, like `run.failed` or `phone.online`. The body's `data`
is a snapshot of that resource. It uses the same field names and formats as the
API, but carries only the fields listed for each event: never your own content
(variables, names, tags), files or download links. See
[Webhooks](/webhooks/overview) for the request format, signatures and delivery
guarantees.

Events about a session, a run or a device you rent carry `device_class`, which
says what kind of device the event concerns. It is `phone` today.

<Info>
  You can send every type below from the dashboard with **Test** on a
  destination's page. A test carries the example payload shown here and
  `"test": true`.
</Info>

## Recognising duplicates

The body's `id` (and the `webhook-id` header) is the same every time one change
reaches you, so you can skip an `id` you have already handled. Axilio builds it
from the event type and the resource:

| Events | `id` |
| - | - |
| `run.*`, `session.started`, `session.ended`, `phone.rental.activated`, `phone.rental.canceled`, `subscription.canceled` | `<type>:<resource id>`. These happen once per resource. |
| `phone.maintenance_*`, `phone.offline`, `phone.online`, `phone.rental.past_due`, `subscription.updated` | `<type>:<resource id>:<time of the change>`. These can happen many times. |
| `session.recording_ready` | `<type>:<session id>:<time the recording was ready>`. One per recording. |
| `invoice.paid`, `auto_recharge.*` | `<type>:<Stripe invoice id>` |
| `invoice.payment_failed` | `<type>:<Stripe invoice id>:<attempt number>` |
| `balance.*` | `<type>:<alert id>`. A new alert opens each time the balance drops again. |
| `file.*` | `<type>:<file or delivery id>` |

## Runs

| Event | Fires when |
| - | - |
| `run.started` | A queued run gets a phone and starts executing. |
| `run.completed` | A run finishes successfully. |
| `run.failed` | A run fails while executing, or Axilio could not queue or dispatch it. Fetch the run for its error message. |
| `run.cancelled` | A run stops before it finishes. `reason` is `user` (you cancelled it) or `queue_timeout` (it waited longer than its start timeout for a phone). |

`data` carries these fields of the [run](/api-reference/rest/runs/get-a-run):
`id`, `workflow_id`, `device_class`, `phone_id`, `session_id`, `status`,
`trigger`, `started_at`, `completed_at`, `canceled_at`, `success` and
`created_at`. A field with no value yet is left out, so a run still waiting for
a phone has no `device_class` or `phone_id`. `run.cancelled` adds `reason`. Fetch the run for
anything else, such as its variables, error message or recording.

```json theme={null}
{
  "id": "run.cancelled:0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
  "type": "run.cancelled",
  "occurred_at": "2026-09-24T12:00:05Z",
  "test": false,
  "data": {
    "id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
    "workflow_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a22",
    "status": "cancelled",
    "trigger": "api",
    "canceled_at": "2026-09-24T12:00:05Z",
    "created_at": "2026-09-24T11:59:57Z",
    "reason": "user"
  }
}
```

The `data` for each run event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="run.cancelled">
    ```json theme={null}
    {
      "id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
      "workflow_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a22",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "cancelled",
      "trigger": "api",
      "started_at": "2026-09-24T12:00:00Z",
      "canceled_at": "2026-09-24T12:00:05Z",
      "created_at": "2026-09-24T11:59:57Z",
      "reason": "user"
    }
    ```
  </Accordion>

  <Accordion title="run.completed">
    ```json theme={null}
    {
      "id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
      "workflow_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a22",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "completed",
      "trigger": "api",
      "started_at": "2026-09-24T12:00:00Z",
      "completed_at": "2026-09-24T12:00:45Z",
      "success": true,
      "created_at": "2026-09-24T11:59:57Z"
    }
    ```
  </Accordion>

  <Accordion title="run.failed">
    ```json theme={null}
    {
      "id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
      "workflow_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a22",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "failed",
      "trigger": "api",
      "started_at": "2026-09-24T12:00:00Z",
      "completed_at": "2026-09-24T12:00:12Z",
      "success": false,
      "created_at": "2026-09-24T11:59:57Z"
    }
    ```
  </Accordion>

  <Accordion title="run.started">
    ```json theme={null}
    {
      "id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a11",
      "workflow_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a22",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "running",
      "trigger": "api",
      "started_at": "2026-09-24T12:00:00Z",
      "created_at": "2026-09-24T11:59:57Z"
    }
    ```
  </Accordion>
</AccordionGroup>

## Sessions

| Event | Fires when |
| - | - |
| `session.started` | A session gets a phone and goes live. |
| `session.ended` | A session ends and Axilio releases its phone. `end_reason` says why. |
| `session.recording_ready` | The session's recording finishes uploading and you can download it. |

`end_reason` is one of:

* **`released`**: you released the session, or Axilio released it for you (a
  connection that never finished setting up, or maintenance on the rack).
* **`idle_timeout`**: no one used the session for longer than its
  [idle timeout](/devices/timeouts).
* **`max_duration`**: the session reached the maximum session length.
* **`run_finished`**: the run that the session served finished.
* **`device_lost`**: the phone stopped answering during the session.

`data` for `session.started` and `session.ended` carries these fields of the
[session](/devices/timeouts): `session_id`, `status`, `source`,
`device_class`, `phone_id`, `workflow_id`, `allocated_at`, `deallocated_at` and
`is_dedicated_phone`. A field with no value yet is left out.
`session.recording_ready` carries `session_id`, `device_class` and `phone_id`;
fetch the recording with
`GET /phones/sessions/{session_id}/recording`.

```json theme={null}
{
  "id": "session.ended:0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
  "type": "session.ended",
  "occurred_at": "2026-09-24T12:05:00Z",
  "test": false,
  "data": {
    "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
    "status": "completed",
    "source": "interactive",
    "device_class": "phone",
    "phone_id": "k3Vq8ZrT2mXa",
    "allocated_at": "2026-09-24T12:00:00Z",
    "deallocated_at": "2026-09-24T12:05:00Z",
    "is_dedicated_phone": false,
    "end_reason": "idle_timeout"
  }
}
```

The `data` for each session event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="session.ended">
    ```json theme={null}
    {
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "completed",
      "source": "interactive",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "allocated_at": "2026-09-24T12:00:00Z",
      "deallocated_at": "2026-09-24T12:05:00Z",
      "is_dedicated_phone": false,
      "end_reason": "released"
    }
    ```
  </Accordion>

  <Accordion title="session.recording_ready">
    ```json theme={null}
    {
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa"
    }
    ```
  </Accordion>

  <Accordion title="session.started">
    ```json theme={null}
    {
      "session_id": "0199a3c2-58c1-7f3a-9f2e-2f9a6a2f6a33",
      "status": "active",
      "source": "interactive",
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "allocated_at": "2026-09-24T12:00:00Z",
      "is_dedicated_phone": false
    }
    ```
  </Accordion>
</AccordionGroup>

## Billing

| Event | Fires when |
| - | - |
| `balance.low` | Your balance drops below the low-balance alert threshold you set. |
| `balance.depleted` | Your balance goes below zero. |
| `balance.recovered` | A balance alert ends because the balance rose back above its threshold. Turning an alert off does not send this. |
| `invoice.paid` | You pay a subscription or rental invoice. |
| `invoice.payment_failed` | A payment attempt on a subscription or rental invoice fails. Each attempt is its own event. |
| `subscription.updated` | Your plan subscription changes status, for example to `past_due` or back to `active`. |
| `subscription.canceled` | Your plan subscription ends. |
| `auto_recharge.succeeded` | Axilio charges an automatic top-up and credits your balance. |
| `auto_recharge.failed` | An automatic top-up fails. Axilio turns auto-recharge off until you turn it back on. |

Amounts use the same units as the [billing API](/billing/balance-and-invoices):
balances in microdollars with a formatted `balance_display`, invoices in cents.

```json theme={null}
{
  "id": "balance.low:0199a3c2-9a0b-7c1d-8e2f-3a4b5c6d7e8f",
  "type": "balance.low",
  "occurred_at": "2026-09-24T12:00:00Z",
  "test": false,
  "data": {
    "alert_id": "0199a3c2-9a0b-7c1d-8e2f-3a4b5c6d7e8f",
    "kind": "low_balance",
    "threshold_microdollars": 5000000,
    "balance_microdollars": 3200000,
    "balance_display": "$3.20"
  }
}
```

The `data` for each billing event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="auto_recharge.failed">
    ```json theme={null}
    {
      "stripe_invoice_id": "in_1PxSample000000000000004",
      "amount_microdollars": 10000000
    }
    ```
  </Accordion>

  <Accordion title="auto_recharge.succeeded">
    ```json theme={null}
    {
      "stripe_invoice_id": "in_1PxSample000000000000003",
      "amount_microdollars": 10000000,
      "balance_microdollars": 10000000,
      "balance_display": "$10.00"
    }
    ```
  </Accordion>

  <Accordion title="balance.depleted">
    ```json theme={null}
    {
      "alert_id": "019fb5fa-1111-7000-8000-000000000002",
      "kind": "negative_balance",
      "threshold_microdollars": 0,
      "balance_microdollars": -1500000,
      "balance_display": "$-1.50"
    }
    ```
  </Accordion>

  <Accordion title="balance.low">
    ```json theme={null}
    {
      "alert_id": "019fb5fa-1111-7000-8000-000000000001",
      "kind": "low_balance",
      "threshold_microdollars": 5000000,
      "balance_microdollars": 3200000,
      "balance_display": "$3.20"
    }
    ```
  </Accordion>

  <Accordion title="balance.recovered">
    ```json theme={null}
    {
      "alert_id": "019fb5fa-1111-7000-8000-000000000001",
      "kind": "low_balance",
      "threshold_microdollars": 5000000,
      "balance_microdollars": 8000000,
      "balance_display": "$8.00"
    }
    ```
  </Accordion>

  <Accordion title="invoice.paid">
    ```json theme={null}
    {
      "stripe_invoice_id": "in_1PxSample000000000000001",
      "invoice_number": "INV-0001",
      "amount_cents": 2900,
      "currency": "usd",
      "status": "paid",
      "plan_name": "Pro",
      "invoice_date": "2026-09-01T12:00:00Z",
      "paid_at": "2026-09-01T12:00:05Z"
    }
    ```
  </Accordion>

  <Accordion title="invoice.payment_failed">
    ```json theme={null}
    {
      "stripe_invoice_id": "in_1PxSample000000000000002",
      "invoice_number": "INV-0002",
      "amount_cents": 2900,
      "currency": "usd",
      "status": "failed",
      "plan_name": "Pro",
      "invoice_date": "2026-09-01T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="subscription.canceled">
    ```json theme={null}
    {
      "subscription_id": "019fb5fa-2222-7000-8000-000000000001",
      "plan_name": "Pro",
      "billing_cycle": "monthly",
      "status": "canceled",
      "current_period_start": "2026-09-01T00:00:00Z",
      "current_period_end": "2026-10-01T00:00:00Z",
      "cancel_at_period_end": true
    }
    ```
  </Accordion>

  <Accordion title="subscription.updated">
    ```json theme={null}
    {
      "subscription_id": "019fb5fa-2222-7000-8000-000000000001",
      "plan_name": "Pro",
      "billing_cycle": "monthly",
      "status": "past_due",
      "current_period_start": "2026-09-01T00:00:00Z",
      "current_period_end": "2026-10-01T00:00:00Z",
      "cancel_at_period_end": false
    }
    ```
  </Accordion>
</AccordionGroup>

## Files

| Event | Fires when |
| - | - |
| `file.ready` | An upload completes, or a file a phone captured during a session lands in your library. `source` is `upload` or `capture`. |
| `file.capture_failed` | A file a phone captured never became a library file. `reason` says why, for example that it was over the size limit or your storage quota. |
| `file.delivered` | A file you pushed to a phone arrived on it. |
| `file.delivery_failed` | A file you pushed to a phone did not arrive. `error` says why. |

`file.ready` and `file.capture_failed` carry the file's `id`, `filename`,
`mime_type`, `size_bytes` and, for a capture, `session_id`. `file.ready` adds
`source`; `file.capture_failed` adds `reason`. The delivery events carry the
[delivery](/files/move-files): `id`, `file_id`, `phone_id`, `filename`,
`mime_type`, `size_bytes`, `status`, `bytes_transferred`, `error` and
`created_at`. No event contains the file itself or a download link; list the
file through the API to get one.

The `data` for each file event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="file.capture_failed">
    ```json theme={null}
    {
      "id": "018f3b1a-8c2d-70a5-9f39-6f1b6b6c9e22",
      "filename": "capture-8c2d70a5.mp4",
      "mime_type": "video/mp4",
      "size_bytes": 734003200,
      "session_id": "018f3aef-5b0a-7e4a-8b0d-3d2c9a7e5f10",
      "reason": "file is 734003200 bytes; the per-capture ceiling is 104857600"
    }
    ```
  </Accordion>

  <Accordion title="file.delivered">
    ```json theme={null}
    {
      "id": "018f3b1b-1a2b-7e11-9c3d-5e6f7a8b9c0d",
      "file_id": "018f3b1a-6e3e-7c9a-9b2f-2a6e1d0c4a11",
      "phone_id": "phone_01H8X9J2K3M4N5P6Q7R8S9T0V1",
      "filename": "screenshot.png",
      "mime_type": "image/png",
      "size_bytes": 245678,
      "status": "delivered",
      "created_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="file.delivery_failed">
    ```json theme={null}
    {
      "id": "018f3b1b-2b3c-7e22-9d4e-6f7a8b9c0d1e",
      "file_id": "018f3b1a-6e3e-7c9a-9b2f-2a6e1d0c4a11",
      "phone_id": "phone_01H8X9J2K3M4N5P6Q7R8S9T0V1",
      "filename": "screenshot.png",
      "mime_type": "image/png",
      "size_bytes": 245678,
      "status": "failed",
      "error": "phone did not acknowledge the push (offline or not yet updated); the file is retained - retry later",
      "created_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="file.ready">
    ```json theme={null}
    {
      "id": "018f3b1a-6e3e-7c9a-9b2f-2a6e1d0c4a11",
      "filename": "screenshot.png",
      "mime_type": "image/png",
      "size_bytes": 245678,
      "source": "upload"
    }
    ```
  </Accordion>
</AccordionGroup>

## Phones

Axilio sends these events only for phones your organization
[rents](/devices/dedicated). Shared phones never send them.

| Event | Fires when |
| - | - |
| `phone.maintenance_started` | The phone leaves service for an automatic repair or a software update. |
| `phone.maintenance_ended` | The repair or update finished and the phone is back. |
| `phone.offline` | The phone leaves service until it proves healthy again or an Axilio operator repairs it. |
| `phone.online` | A phone that was out of service is back. |
| `phone.rental.activated` | A rental starts and the phone becomes yours alone. |
| `phone.rental.past_due` | A rental payment fails. The phone stays yours while Stripe retries the payment. |
| `phone.rental.canceled` | A rental subscription ends. Axilio releases the phone once no session is running on it. |

The health events carry `device_class` (`phone`), `phone_id`, `phone_name`,
`nickname`, `status`, `reason` and `changed_at`. `reason` on the maintenance and
offline events is one of `hardware`, `update`, `unresponsive` or `operator`. The rental events carry the rental as
`GET /billing/phone-rental-subscriptions` returns it.

```json theme={null}
{
  "id": "phone.offline:k3Vq8ZrT2mXa:1790251200000000",
  "type": "phone.offline",
  "occurred_at": "2026-09-24T12:00:00Z",
  "test": false,
  "data": {
    "device_class": "phone",
    "phone_id": "k3Vq8ZrT2mXa",
    "phone_name": "Pixel 9 #14",
    "nickname": "checkout-bot",
    "status": "out_of_service",
    "reason": "unresponsive",
    "changed_at": "2026-09-24T12:00:00Z"
  }
}
```

The `data` for each phone event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="phone.maintenance_ended">
    ```json theme={null}
    {
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "nickname": "checkout-bot",
      "status": "active",
      "changed_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="phone.maintenance_started">
    ```json theme={null}
    {
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "nickname": "checkout-bot",
      "status": "recovering",
      "reason": "hardware",
      "changed_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="phone.offline">
    ```json theme={null}
    {
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "nickname": "checkout-bot",
      "status": "out_of_service",
      "reason": "unresponsive",
      "changed_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="phone.online">
    ```json theme={null}
    {
      "device_class": "phone",
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "nickname": "checkout-bot",
      "status": "active",
      "changed_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="phone.rental.activated">
    ```json theme={null}
    {
      "id": "0199a3c2-7d1e-7f00-8a11-2b3c4d5e6f70",
      "organization_id": "0199a3c2-1111-7000-8000-000000000001",
      "plan_id": "0199a3c2-2222-7000-8000-000000000002",
      "plan_name": "Dedicated phone, monthly",
      "plan_interval": "month",
      "plan_price_cents": 29900,
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "phone_nickname": "checkout-bot",
      "status": "active",
      "payment_status": "healthy",
      "current_period_start": "2026-09-01T00:00:00Z",
      "current_period_end": "2026-10-01T00:00:00Z",
      "cancel_at_period_end": false
    }
    ```
  </Accordion>

  <Accordion title="phone.rental.canceled">
    ```json theme={null}
    {
      "id": "0199a3c2-7d1e-7f00-8a11-2b3c4d5e6f70",
      "organization_id": "0199a3c2-1111-7000-8000-000000000001",
      "plan_id": "0199a3c2-2222-7000-8000-000000000002",
      "plan_name": "Dedicated phone, monthly",
      "plan_interval": "month",
      "plan_price_cents": 29900,
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "phone_nickname": "checkout-bot",
      "status": "canceled",
      "payment_status": "healthy",
      "current_period_start": "2026-09-01T00:00:00Z",
      "current_period_end": "2026-10-01T00:00:00Z",
      "cancel_at_period_end": false,
      "canceled_at": "2026-09-24T12:00:00Z"
    }
    ```
  </Accordion>

  <Accordion title="phone.rental.past_due">
    ```json theme={null}
    {
      "id": "0199a3c2-7d1e-7f00-8a11-2b3c4d5e6f70",
      "organization_id": "0199a3c2-1111-7000-8000-000000000001",
      "plan_id": "0199a3c2-2222-7000-8000-000000000002",
      "plan_name": "Dedicated phone, monthly",
      "plan_interval": "month",
      "plan_price_cents": 29900,
      "phone_id": "k3Vq8ZrT2mXa",
      "phone_name": "Pixel 9 #14",
      "phone_nickname": "checkout-bot",
      "status": "active",
      "payment_status": "past_due",
      "current_period_start": "2026-09-01T00:00:00Z",
      "current_period_end": "2026-10-01T00:00:00Z",
      "cancel_at_period_end": false
    }
    ```
  </Accordion>
</AccordionGroup>

## Platform

| Event | Fires when |
| - | - |
| `webhook.test` | Only when you send it. Use it to check that a new endpoint receives and verifies deliveries. |

The `data` for each platform event, exactly as a **Test** delivery sends it:

<AccordionGroup>
  <Accordion title="webhook.test">
    ```json theme={null}
    {
      "message": "This is a test event from Axilio."
    }
    ```
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bell" href="/webhooks/overview">
    Register a destination and verify signatures.
  </Card>

  <Card title="Monitor runs" icon="diagram-project" href="/workflows/monitor">
    What each run status means.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.