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

# Webhooks

> Receive signed, real-time HTTP callbacks when tasks, workflows, the assistant, phone numbers, or mailboxes change state in your Mobilerun account.

A webhook is an HTTPS POST that Mobilerun sends to a URL you own when something happens in your account. Instead of polling the API, your server (or an automation tool such as n8n, Zapier, or Make) receives the event within seconds.

Every delivery is signed with a per-endpoint secret, retried on transient failures, and recorded so you can inspect what was sent and what your server answered.

<Tip>
  Want to be pinged when the Mobilerun assistant finishes a job, fails, or is waiting for your answer? Follow the [assistant notifications guide](/assistant/notifications). You can even ask the assistant to set the webhook up for you.
</Tip>

## How events reach you

One webhook endpoint can be fed in three different ways. They share the same URL, secret, signature, and delivery log, but they are configured in different places.

| Mode | Where you configure it | What arrives |
| - | - | - |
| **Platform subscription** | Webhook endpoint → **Events** filter | Every event in the catalog below whose type matches your filter, for the whole organization |
| **Workflow completion notification** | Workflow settings → **Notify on success / failure** | One `triggers.notification` per finished run of that workflow, sent only to the chosen endpoint |
| **Workflow result delivery** | A `webhooks.send` step inside a workflow | One `workflow.step.emit` carrying data the workflow author chose, sent only to the chosen endpoint |

The two workflow modes are *targeted*: they always reach the endpoint named in the workflow, regardless of that endpoint's event filter. Platform subscriptions are *fan-out*: the filter decides which events you receive.

## Creating a webhook

In the dashboard open **Webhooks** and click **Add Webhook**. Via the API, `POST /v1/webhooks` with the same fields.

1. Enter a public HTTPS URL. Mobilerun never follows redirects, so point at the final URL.
2. Pick the event types you want. Leaving the filter empty subscribes the endpoint to every event, which is rarely what you want.
3. Save. The response shows the **signing secret exactly once**. Store it in your receiver's configuration now; it cannot be read back later.
4. Click **Send test** (or `POST /v1/webhooks/{id}/test`). A signed `webhook.test` event is delivered immediately and appears in the delivery log.

Limits: 50 endpoints per organization, and the same active URL cannot be registered twice.

### Managing endpoints

| Action | Dashboard | API |
| - | - | - |
| Change events, description, or state | Edit webhook | `PATCH /v1/webhooks/{id}` |
| Rotate the secret | Rotate secret | `POST /v1/webhooks/{id}/rotate-secret` |
| Inspect deliveries and attempts | Deliveries tab | `GET /v1/webhooks/{id}/deliveries` |
| Delete | Delete webhook | `DELETE /v1/webhooks/{id}` |

Rotating the secret invalidates the old one immediately. Update your receiver first, then rotate, or accept a short window of rejected deliveries.

### States and health

| Field | Values | Meaning |
| - | - | - |
| **State** | `ACTIVE`, `DISABLED` | You control this. Disabled endpoints receive nothing. Deleting an endpoint is a separate, one-way step. |
| **Health** | `healthy`, `failing`, `blocked` | Mobilerun observes this from your responses. One failed delivery marks the endpoint `failing` but deliveries continue. When the last five deliveries all failed over a span of at least an hour, or a delivery fails for a reason that can never succeed such as a private IP, the endpoint becomes `blocked` and stops receiving events. Fix the receiver, then set the state back to `ACTIVE` to resume. |

## What your receiver gets

Every delivery is a `POST` with a JSON body in the platform event envelope:

```json theme={null}
{
  "schemaVersion": 1,
  "id": "0d1c4a4e-4a1b-5e4e-9d0a-8a8d0d9f1b3c",
  "source": "tasks-api",
  "type": "task.run.completed",
  "userId": "…",
  "ownerId": "…",
  "createdBy": "…",
  "occurredAt": "2026-09-04T10:52:13.412Z",
  "data": { }
}
```

| Field | Description |
| - | - |
| `id` | Stable event id. A retried delivery reuses the same id, so deduplicate on it. |
| `source` | Origin identifier of the event (for example `tasks-api`). Informational only: route on `type`, not on `source`; new values may appear. |
| `type` | Event type from the catalog below. |
| `ownerId` | The organization the event belongs to. |
| `userId`, `createdBy` | The acting user. `createdBy` is `null` for system-initiated events. |
| `occurredAt` | ISO 8601 timestamp of the event. |
| `data` | Event-specific payload. Shapes are documented per event type below. |

### Headers

| Header | Description |
| - | - |
| `X-Mobilerun-Event-Id` | Same as the body `id`. |
| `X-Mobilerun-Event-Type` | Same as the body `type`. |
| `X-Mobilerun-Delivery-Id` | Unique per delivery attempt chain; useful for support questions. |
| `X-Mobilerun-Source` | Same as the body `source`. |
| `X-Mobilerun-Timestamp` | Unix time in **milliseconds** when the request was signed. |
| `X-Mobilerun-Signature` | `sha256=<hex>`, see below. |

### Verifying the signature

The signature is an HMAC-SHA256 over the string `"{timestamp}.{rawBody}"` using your endpoint secret, where `timestamp` is the value of `X-Mobilerun-Timestamp` and `rawBody` is the request body exactly as received. Verify before you parse the JSON.

```python theme={null}
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = "…"  # the secret shown once when you created the webhook

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    ts = request.headers.get("X-Mobilerun-Timestamp", "")
    sig = request.headers.get("X-Mobilerun-Signature", "")
    raw = request.get_data()  # bytes, unmodified

    # Reject stale requests (replay protection).
    if abs(time.time() * 1000 - int(ts or 0)) > 5 * 60 * 1000:
        abort(400)

    expected = "sha256=" + hmac.new(
        SECRET.encode(), f"{ts}.".encode() + raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, sig):
        abort(401)

    event = request.get_json()
    # Deduplicate on event["id"] before doing any work.
    return "", 200
```

<Note>
  For a complete, runnable receiver with HMAC verification, replay protection, and secure file downloads, see the
  [Python webhook receiver example](https://github.com/droidrun/mobilerun-examples/tree/main/examples/python/webhook-file-receiver).
</Note>

### Delivery and retries

| Rule | Value |
| - | - |
| Request timeout | 10 seconds |
| Attempts | 3, with exponential backoff and jitter, capped at 5 minutes between attempts |
| Retried responses | Any `5xx`, plus `408`, `409`, `425`, `429`, and network errors |
| Terminal responses | Any other `4xx`. The delivery is marked failed and not retried. |
| Redirects | Never followed. A `3xx` counts as a failed attempt. |
| Deduplication | One delivery per `(event id, endpoint)`. Re-emitted events collapse into the existing delivery. |
| Maximum age | Events older than 24 hours are dropped instead of being delivered late. |

Respond with a `2xx` as soon as you have persisted the event, and do the actual work asynchronously. Slow receivers hit the timeout and burn attempts.

## Event catalog

Use these identifiers in the endpoint's event filter. The assistant and the SDK can list the live catalog for you; the table reflects the catalog at the time of writing.

### Tasks

| Type | Description |
| - | - |
| `task.run.queued` | Task accepted and queued for dispatch. |
| `task.run.created` | Task dispatched to a worker. |
| `task.run.running` | Task started executing. |
| `task.run.paused` | Task was paused. |
| `task.run.completed` | Task finished successfully. |
| `task.run.failed` | Task ended in failure. |
| `task.run.cancelling` | Cancellation requested for a running task. |
| `task.run.cancelled` | Task was cancelled. |

### Assistant (interactive chat)

These fire for the hosted Mobilerun assistant, independent of whether you still have the chat open. See the [assistant notifications guide](/assistant/notifications) for payloads and setup.

| Type | Description |
| - | - |
| `chat.turn.completed` | An assistant turn finished successfully. |
| `chat.turn.failed` | An assistant turn ended in failure. Turns you stop, and turns stopped by a budget, send nothing. |
| `chat.input.required` | The assistant asked a question and is waiting for your answer. |

### Workflows

| Type | Description |
| - | - |
| `workflow.run.queued` | A workflow execution was queued for dispatch. |
| `workflow.run.running` | A workflow execution started executing. |
| `workflow.run.completed` | A workflow execution finished successfully. |
| `workflow.run.failed` | A workflow execution ended in failure. |
| `workflow.run.cancelled` | A workflow execution was cancelled. |
| `workflow.run.invalid` | A workflow execution was rejected as invalid. |
| `triggers.notification` | Notify-only message from a workflow: the automatic **Notify on success / failure** setting, or a notification step the author added. |
| `workflow.step.emit` | Custom payload from an explicit `webhooks.send` step; `data` is whatever the workflow author configured. |
| `agent.run.completed` | An agent run started by a workflow finished successfully. |
| `agent.run.failed` | An agent run started by a workflow ended in failure. |
| `agent.run.timed_out` | An agent run started by a workflow exceeded its time limit. |

`triggers.notification` always has this `data` shape:

```json theme={null}
{
  "title": "Workflow \"Daily scrape\" succeeded",
  "message": "optional free text",
  "severity": "success",
  "category": "optional",
  "link": "https://…",
  "tags": ["optional"],
  "attributes": { "key": "value" },
  "origin": {
    "flowId": "…",
    "triggerId": "…",
    "executionId": "…",
    "sourceEventId": "…",
    "deviceId": "…"
  }
}
```

`severity` is one of `info`, `success`, `warning`, `error`. The automatic completion notification sets `title` to `Workflow "<name>" succeeded` or `Workflow "<name>" failed` and `severity` to `success` or `error`. `origin` lets you correlate the notification with the run in the dashboard or via the API.

### Phone numbers and eSIMs

| Type | Description |
| - | - |
| `sms.received` | An inbound SMS was recorded for a device's number. |
| `sms.send.sent` | An outbound SMS reached a terminal sent state. |
| `sms.send.delivered` | An outbound SMS was confirmed delivered. |
| `sms.send.failed` | An outbound SMS ended in a terminal failed state. |
| `esim.install.completed` | An eSIM finished installing onto a device. |
| `esim.install.failed` | An eSIM install ended in a terminal failure. |
| `number.purchase.completed` | A phone number purchase completed and the number became active. |
| `number.purchase.failed` | A phone number purchase failed and was compensated. |
| `esim.purchase.completed` | An eSIM was purchased from stock. |

### Mailboxes

| Type | Description |
| - | - |
| `mailbox.message.received` | An inbound mailbox message was recorded. |
| `mailbox.rent.activated` | A mailbox rental became active after payment confirmation. |

### Diagnostics

| Type | Description |
| - | - |
| `webhook.test` | Synthetic delivery triggered by **Send test**. |

## Integrations

<CardGroup cols={2}>
  <Card title="n8n" icon="workflow" href="/n8n">
    Start an n8n workflow from a Mobilerun webhook using the Webhook trigger node, then act on the event with the official Mobilerun node.
  </Card>

  <Card title="Zapier and Make" icon="bolt">
    Use a "Catch Hook" or "Custom webhook" trigger. Both tools can forward the event as an email, Slack message, or push notification.
  </Card>

  <Card title="Alerting" icon="bell">
    Subscribe to `task.run.failed`, `workflow.run.failed`, and `chat.turn.failed` and route them to your on-call channel.
  </Card>

  <Card title="Data pipelines" icon="database">
    Store `task.run.completed` results, inbound SMS, and mailbox messages in your own database as they arrive.
  </Card>
</CardGroup>

## Best practices

| Practice | Description |
| - | - |
| **Verify every request** | Check the signature and the timestamp before trusting the body. |
| **Deduplicate on event id** | Retries and re-emissions reuse the same `id`. |
| **Acknowledge fast** | Return `2xx` within a few hundred milliseconds and process asynchronously. |
| **Filter narrowly** | Subscribe to the event types you handle. Everything else costs you deliveries and attempts. |
| **Watch health** | A `blocked` endpoint silently receives nothing until you re-enable it. Alert on it. |


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