Webhook event types
Each webhook 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 | An allocate request was accepted; the device may still be provisioning |
allocation.ready | A device is allocated and usable |
allocation.failed | No device was allocated |
allocation.released | A device left the session |
usage.threshold_reached | Usage crossed 80% or 100% of the period's included seconds |
usage.quota_exceeded | An allocate was refused because the usage quota is exhausted |
app.uploaded | An uploaded app was verified and can be installed |
app.upload_failed | An uploaded app failed verification |
app.deleted | An uploaded app was deleted |
test_run.started | A test result was created |
test_run.finished | A test run reached passed, failed, flaky or errored |
webhook.test | 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
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. |
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
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
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
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
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
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
resource is the metered resource, today mobile. periodStart and periodEnd bound the
current billing period.
usage.threshold_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
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
Sent for builds uploaded through Apps in the web app or the /files API. See
Apps.
app.uploaded
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
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
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
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
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
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
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"
}
}