# Webhook event types

Each [webhook](https://mobilenext.ai/docs/webhooks.md) request body is one event. The envelope is the same for every type; `data` depends on `type`. Ids are UUIDs. Times are ISO 8601 in UTC. New fields can be added to `data` at any time, and the enums below can gain values, so ignore what you don't know.

| Type                                                 | Sent when                                                              |
| ---------------------------------------------------- | ---------------------------------------------------------------------- |
| [`allocation.created`](#allocationcreated)           | An allocate request was accepted; the device may still be provisioning |
| [`allocation.ready`](#allocationready)               | A device is allocated and usable                                       |
| [`allocation.failed`](#allocationfailed)             | No device was allocated                                                |
| [`allocation.released`](#allocationreleased)         | A device left the session                                              |
| [`usage.threshold_reached`](#usagethreshold_reached) | Usage crossed 80% or 100% of the period's included seconds             |
| [`usage.quota_exceeded`](#usagequota_exceeded)       | An allocate was refused because the usage quota is exhausted           |
| [`app.uploaded`](#appuploaded)                       | An uploaded app was verified and can be installed                      |
| [`app.upload_failed`](#appupload_failed)             | An uploaded app failed verification                                    |
| [`app.deleted`](#appdeleted)                         | An uploaded app was deleted                                            |
| [`test_run.started`](#test_runstarted)               | A test result was created                                              |
| [`test_run.finished`](#test_runfinished)             | A test run reached passed, failed, flaky or errored                    |
| [`webhook.test`](#webhooktest)                       | You sent a test event                                                  |

When you add a webhook, pick exact types, a group such as `allocation.*` (which also covers types added to the group later), or all events (`*`). `GET /api/v1/webhooks/event-types` lists the types you can pick. `webhook.test` is not in it: it goes only to the webhook you test.

## Allocation events[​](#allocation-events "Direct link to Allocation events")

An allocation is one device in one session, from the request until it is released. Every `allocation.*` event carries these fields in `data`:

| Field              |                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `allocation`       | The allocation id; the same as `device.id`. Join the events of one allocation on it.                     |
| `allocationStatus` | `provisioning`, `ready`, `released` or `failed`. See [Lifecycle rank](#lifecycle-rank).                  |
| `session`          | The session the allocation belongs to. Absent when the request named none.                               |
| `user`             | The user who requested the device.                                                                       |
| `device`           | The device, as `GET /api/v1/sessions/{sessionId}/devices` returns it. Absent until a device is assigned. |

### Lifecycle rank[​](#lifecycle-rank "Direct link to Lifecycle rank")

Events are delivered in no particular order, so a late `allocation.ready` can arrive after the `allocation.released` of the same allocation. Rank each event by its `allocationStatus`:

| `allocationStatus`   | Rank      |
| -------------------- | --------- |
| `provisioning`       | 1         |
| `ready`              | 2         |
| `released`, `failed` | 3 (final) |

To track an allocation's current state, keep the highest rank you have seen per `allocation`: an event with a lower rank never moves it back. Still process that event for anything else it tells you, such as when the device became ready. A final state never changes. When in doubt, read the session's devices from the API.

### `allocation.created`[​](#allocationcreated "Direct link to allocationcreated")

An allocate request was accepted. `data.allocationStatus` is `provisioning` while a device is being prepared, or `ready` when one was available at once; in that case `allocation.ready` is sent too. `data.status` repeats the same value on this event only. `data.requestedAt` is when the request arrived.

```
{

  "id": "dfea3d5e-5a36-492b-885e-aab6cffdec2c",

  "object": "event",

  "type": "allocation.created",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "allocation": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

    "session": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",

    "user": "9c84e1f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",

    "allocationStatus": "provisioning",

    "status": "provisioning",

    "requestedAt": "2026-09-22T09:59:31.000Z"

  }

}
```

### `allocation.ready`[​](#allocationready "Direct link to allocationready")

The device is usable. `data.device` is present, and `data.waitSeconds` is the time from the request to ready.

```
{

  "id": "7d368364-6535-417d-bade-8a75ecf93cac",

  "object": "event",

  "type": "allocation.ready",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "allocation": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

    "session": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",

    "user": "9c84e1f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",

    "allocationStatus": "ready",

    "device": {

      "id": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

      "status": "in_use",

      "info": {

        "platform": "ios",

        "type": "real",

        "name": "Apple iPhone 16 Pro",

        "osVersion": "18.4",

        "serial": "00008150-001C30842678401C"

      },

      "createdAt": "2026-09-22T10:00:04.000Z"

    },

    "waitSeconds": 33

  }

}
```

### `allocation.failed`[​](#allocationfailed "Direct link to allocationfailed")

No device was allocated. `data.reason` is one of `no_device_available`, `quota_exceeded`, `concurrency_limit`, `provision_failed`, `provision_timeout` or `internal`. When the allocate request itself was refused, `data.filters` holds its filters, and `allocation` is a fresh id made for the event because no allocation exists.

```
{

  "id": "ef7238ef-580a-4457-97c0-70e33a465574",

  "object": "event",

  "type": "allocation.failed",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "allocation": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

    "session": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",

    "user": "9c84e1f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",

    "allocationStatus": "failed",

    "reason": "no_device_available",

    "filters": [

      {

        "attribute": "platform",

        "operator": "EQUALS",

        "value": "ios"

      }

    ]

  }

}
```

### `allocation.released`[​](#allocationreleased "Direct link to allocationreleased")

The device left the session.

| Field             |                                                                                        |
| ----------------- | -------------------------------------------------------------------------------------- |
| `reason`          | `user` (released by you or your code), `idle`, `host_disconnected`, or `stale_record`. |
| `durationSeconds` | Seconds from ready to release.                                                         |
| `billedSeconds`   | Seconds billed for this allocation.                                                    |
| `releasedAt`      | When the release happened.                                                             |

`stale_record` means Mobile Next repaired a record of a device it had lost track of. The device's real end time is unknown, `billedSeconds` is 0, and `releasedAt` is when the record was repaired.

```
{

  "id": "50af8bdc-39bb-4a39-a56b-e90434bcfa05",

  "object": "event",

  "type": "allocation.released",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "allocation": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

    "session": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",

    "user": "9c84e1f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",

    "allocationStatus": "released",

    "device": {

      "id": "7f0c9a1e-2b3d-4c5e-8f90-a1b2c3d4e5f6",

      "status": "released",

      "info": {

        "platform": "ios",

        "type": "real",

        "name": "Apple iPhone 16 Pro",

        "osVersion": "18.4",

        "serial": "00008150-001C30842678401C"

      },

      "createdAt": "2026-09-22T10:00:04.000Z",

      "releasedAt": "2026-09-22T10:05:00.117Z"

    },

    "reason": "user",

    "durationSeconds": 296,

    "billedSeconds": 300,

    "releasedAt": "2026-09-22T10:05:00.117Z"

  }

}
```

## Usage events[​](#usage-events "Direct link to Usage events")

`resource` is the metered resource, today `mobile`. `periodStart` and `periodEnd` bound the current billing period.

### `usage.threshold_reached`[​](#usagethreshold_reached "Direct link to usagethreshold_reached")

Usage crossed 80% or 100% of the seconds your plan includes for the period. Sent once per threshold, resource and period; one session can cross both thresholds at once and send two events. `secondsIncluded` is your plan's grant for the period, without coupons. `secondsRemaining` is the balance after the usage, coupons included. Organizations without an included amount get no threshold events.

```
{

  "id": "a5082316-da84-4b69-b2cf-b83c9a53e24d",

  "object": "event",

  "type": "usage.threshold_reached",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "resource": "mobile",

    "thresholdPercent": 80,

    "secondsIncluded": 360000,

    "secondsRemaining": 71940,

    "periodStart": "2026-09-01T00:00:00Z",

    "periodEnd": "2026-10-01T00:00:00Z"

  }

}
```

### `usage.quota_exceeded`[​](#usagequota_exceeded "Direct link to usagequota_exceeded")

An allocate request was refused because the organization is out of seconds. Sent once per resource and period.

```
{

  "id": "38904bfa-d737-4f40-a9e4-070549362f06",

  "object": "event",

  "type": "usage.quota_exceeded",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "resource": "mobile",

    "periodStart": "2026-09-01T00:00:00Z",

    "periodEnd": "2026-10-01T00:00:00Z"

  }

}
```

## App events[​](#app-events "Direct link to App events")

Sent for builds uploaded through **Apps** in the web app or the `/files` API. See [Apps](https://mobilenext.ai/docs/cloud/apps.md).

### `app.uploaded`[​](#appuploaded "Direct link to appuploaded")

An uploaded build was verified and can be installed. `file` is its file id. When the build's bundle id was read, `data` also names the app: `app` (its id), `appName`, `bundleId`, `platform` (`android` or `ios`), `version`, `buildNumber`, and `url`, the app's page in the web app.

```
{

  "id": "624460ae-210e-43ce-abfa-1ed812993a32",

  "object": "event",

  "type": "app.uploaded",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "file": "3e8a9b1c-4d5e-4f6a-8b7c-9d0e1f2a3b4c",

    "filename": "shop.apk",

    "filesize": 48213504,

    "app": "6f186ef6-ff87-465b-924b-24796bb08a54",

    "appName": "Acme Shop",

    "bundleId": "com.acme.shop",

    "platform": "android",

    "version": "4.2.0",

    "buildNumber": "4201",

    "url": "https://app.mobilenext.ai/dashboard/apps/6f186ef6-ff87-465b-924b-24796bb08a54"

  }

}
```

### `app.upload_failed`[​](#appupload_failed "Direct link to appupload_failed")

An uploaded build failed verification. `reason` is the file's `failureReason`, such as `checksum_mismatch`, `size_mismatch`, `unsupported_format` or `unsupported_architecture`.

```
{

  "id": "d88dfe9b-162f-4b8b-adad-bc93ad108615",

  "object": "event",

  "type": "app.upload_failed",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "file": "3e8a9b1c-4d5e-4f6a-8b7c-9d0e1f2a3b4c",

    "filename": "shop.apk",

    "reason": "checksum_mismatch"

  }

}
```

### `app.deleted`[​](#appdeleted "Direct link to appdeleted")

An uploaded build was deleted with **Delete** in the web app or `DELETE /api/v1/files/{fileId}`. A build that reaches its expiry date does not send it. `data` has the same fields as `app.uploaded`.

```
{

  "id": "0b7c4e2a-9f1d-4c3b-8a6e-2d5f7a9c1e3b",

  "object": "event",

  "type": "app.deleted",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-23T08:30:00.412Z",

  "data": {

    "file": "3e8a9b1c-4d5e-4f6a-8b7c-9d0e1f2a3b4c",

    "filename": "shop.apk",

    "filesize": 48213504,

    "app": "6f186ef6-ff87-465b-924b-24796bb08a54",

    "appName": "Acme Shop",

    "bundleId": "com.acme.shop",

    "platform": "android",

    "version": "4.2.0",

    "buildNumber": "4201",

    "url": "https://app.mobilenext.ai/dashboard/apps/6f186ef6-ff87-465b-924b-24796bb08a54"

  }

}
```

## Test run events[​](#test-run-events "Direct link to Test run events")

Sent for test results reported by Mobilewright or the `/test-results` API. `data` is the test result as `GET /api/v1/test-results/{testResultId}` returns it.

### `test_run.started`[​](#test_runstarted "Direct link to test_runstarted")

A test result was created. A reporter that posts when the run begins sends `status: "running"`; one that posts after the run sends its final status, and `test_run.finished` follows at once.

```
{

  "id": "c4b1f0d2-6e3a-4f57-9a0b-2d8e1c7f3a64",

  "object": "event",

  "type": "test_run.started",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-11-03T10:00:00.084Z",

  "data": {

    "id": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",

    "name": "Checkout flow",

    "suite": "shop-app",

    "userAgent": "mobilewright/0.0.39",

    "git": { "repoUrl": "https://github.com/acme/shop-app", "branch": "main", "commitSha": "9f2c1e7" },

    "status": "running",

    "createdAt": "2026-11-03T10:00:00Z"

  }

}
```

### `test_run.finished`[​](#test_runfinished "Direct link to test_runfinished")

A run reached a final status: `passed`, `failed`, `flaky` or `errored`. Sent once per run, with the first final status; a later change to the run's status sends nothing.

`isRecovered` is `true` when this run did not fail and the previous finished run of the same `suite` on the same `git.branch` failed or errored: use it for "fixed" notifications. Runs reported in the same second, such as parallel shards, are not each other's previous run. It is `false` when the run has no suite or no branch.

```
{

  "id": "e9a7d3b5-1f2c-4e8a-b6d0-3c5f7a9e1b42",

  "object": "event",

  "type": "test_run.finished",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-11-03T10:03:04.512Z",

  "data": {

    "id": "7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",

    "name": "Checkout flow",

    "suite": "shop-app",

    "userAgent": "mobilewright/0.0.39",

    "git": { "repoUrl": "https://github.com/acme/shop-app", "branch": "main", "commitSha": "9f2c1e7" },

    "stats": { "startTime": "2026-11-03T10:00:00Z", "duration": 184213, "expected": 42, "unexpected": 0, "skipped": 1, "flaky": 0 },

    "status": "passed",

    "createdAt": "2026-11-03T10:00:00Z",

    "isRecovered": true

  }

}
```

## `webhook.test`[​](#webhooktest "Direct link to webhooktest")

Sent when you choose **Send test event** or call `POST /api/v1/webhooks/{webhookId}/test`, to that webhook only. `data.webhook` is the webhook's id.

```
{

  "id": "4f0a2f60-8b23-4b15-a05d-ba45ad285bf6",

  "object": "event",

  "type": "webhook.test",

  "apiVersion": "2026-09-22",

  "createdAt": "2026-09-22T10:05:00.117Z",

  "data": {

    "webhook": "1d2e3f4a-5b6c-4d7e-8f9a-0b1c2d3e4f5a"

  }

}
```
