> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.islo.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.islo.dev/_mcp/server.

# Incoming webhooks

> Receive external events, route them to sandboxes, and run lifecycle actions automatically.

This advanced platform feature handles sandbox lifecycle and single-Job events. For a multi-stage factory workflow, configure a [Line trigger](/concepts/triggers).

Incoming webhooks let external systems (GitHub, Stripe, your own app) POST events to Islo. Each delivery is authenticated, mapped to a sandbox, and runs a sequence of **actions** — create the sandbox if needed, resume it, forward the payload to a port inside it, pause, delete, or start a **single** deployed job.

For multi-stage workflows (review then implement then verify, scheduled pipelines, GitHub/Slack/Linear-native events with routing), use a factory line trigger instead. See [Automations](/concepts/automations). Incoming webhooks are the right tool when the event should ensure a sandbox, drive lifecycle, or kick one job.

Create a webhook via the SDK or `POST /webhooks/incoming`. The response includes a `receiver_url` — paste that into GitHub, Stripe, or any HTTP client. External callers hit that URL; you manage the config through the authenticated API.

## Authoring formats

Incoming webhook definitions may be authored as **TOML** or **JSON**. The CLI accepts `--request-json` or `--request-toml` (or infers format from a file extension). The web UI accepts either format in the advanced create editor.

At the API and database boundary the config is always **JSON** (`IncomingWebhookCreate` / stored webhook record). TOML is a representation format only; it is parsed to JSON before create or update.

Detail views render the stored configuration as TOML by default, with JSON available as a debug view.

## How a delivery flows

```
External service POST → receiver_url
    │
    ├── Authenticate (HMAC, JWT, basic, etc.)
    ├── Dedupe (idempotency key)
    ├── Resolve target sandbox name (fixed or from event payload)
    │
    └── For each matching rule:
            run actions in order
                ensure_sandbox → resume_sandbox → deliver_to_port → …
```

Management endpoints (all under the compute plane **Webhooks** tag in API Reference):

| Operation  | Purpose                                     |
| ---------- | ------------------------------------------- |
| Create     | Register a receiver; returns `receiver_url` |
| List / Get | Inspect existing receivers                  |
| Update     | Replace auth, target, rules, or status      |
| Delete     | Soft-delete a receiver                      |

Set `status` to `disabled` to stop processing without deleting the config.

## Target resolution

Every delivery targets exactly one sandbox. Choose how the name is determined:

| `target_type`             | Use when                                                                                 |
| ------------------------- | ---------------------------------------------------------------------------------------- |
| `fixed_sandbox_name`      | All events go to one sandbox (`sandbox_name: "staging-receiver"`)                        |
| `sandbox_name_from_event` | Name is extracted from the request via a `source` (`header`, `query`, `json_path`, etc.) |

For `sandbox_name_from_event`, optional guards restrict what names are allowed:

* `required_prefix` — e.g. `pr-` so only `pr-42` style names match
* `allowed_pattern` — regex allowlist
* `allowed_names` — explicit list

Example: derive a sandbox name from a GitHub PR number in the JSON body:

```python
target={
    "target_type": "sandbox_name_from_event",
    "source": {"source": "json_path", "path": "$.pull_request.number"},
    "required_prefix": "pr-",
}
```

## Rules and actions

A webhook has one or more **rules**. Each rule has:

* `when` (optional) — `json_path` + `equals` filter on the payload; omit to run on every delivery
* `actions` — ordered list of steps to run against the resolved sandbox

| `action_type`     | What it does                                                                                                         |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| `trigger_job`     | Start a deployed job run (`job_name`, optional `version_id`, `region`, and `params` mapped from the webhook request) |
| `ensure_sandbox`  | Create the sandbox from `template` if it does not exist; no-op if it already exists                                  |
| `resume_sandbox`  | Resume a paused sandbox                                                                                              |
| `pause_sandbox`   | Pause a running sandbox                                                                                              |
| `delete_sandbox`  | Delete the sandbox                                                                                                   |
| `deliver_to_port` | Forward the webhook payload to a TCP port inside the sandbox (`port`, optional `path`, `payload`, `auto_resume`)     |

**`ensure_sandbox`** is the create-if-missing step. It does not recreate or update an existing sandbox. The `template` mirrors a sandbox create request.

Discover the full template schema via `islo schema webhook` or compute OpenAPI `IncomingWebhookSandboxTemplate`.

| Field                       | Required | Notes                                                                   |
| --------------------------- | -------- | ----------------------------------------------------------------------- |
| `image`                     | yes      | Use `ghcr.io/islo-labs/islo-runner:latest` for the platform default     |
| `vcpus`                     | yes      | Virtual CPUs                                                            |
| `memory_mb`                 | yes      | Memory in MB                                                            |
| `disk_gb`                   | yes      | Disk in GB                                                              |
| `gateway_profile`           | no       | Gateway profile for credential injection                                |
| `workdir`                   | no       | Working directory inside the sandbox                                    |
| `snapshot_name`             | no       | Restore from a named snapshot on create                                 |
| `init`                      | no       | Tagged object: `{ "type": "full" }`, `{ "type": "minimal" }`, or custom |
| `env`                       | no       | Environment variables                                                   |
| `sources` / `setup_scripts` | no       | Git clones and setup scripts                                            |
| `lifecycle`                 | no       | [`lifecycle`](/concepts/sandbox-lifecycle) for auto-pause and delete    |

Typical preview-env chain: `ensure_sandbox` → `deliver_to_port` with `auto_resume: on_activity` so a cold sandbox wakes before the HTTP forward.

## Authentication

`auth` verifies the caller before any rule runs. Supported `auth_type` values:

| `auth_type`                      | Notes                                                  |
| -------------------------------- | ------------------------------------------------------ |
| `none`                           | No verification (use only for testing)                 |
| `hmac`                           | Signature over a signed payload (GitHub, Stripe-style) |
| `header_equals` / `query_equals` | Static shared secret in a header or query param        |
| `basic` / `bearer_static`        | HTTP basic or static bearer token                      |
| `jwt`                            | JWKS-based JWT validation                              |
| `ip_allowlist`                   | Source IP restriction                                  |
| `all` / `any`                    | Combine multiple verifiers                             |

Secrets are passed inline at create time as `{name, value}` pairs inside the verifier config. Responses redact values and return `secret_ref` names only.

## Idempotency

`idempotency` deduplicates retries. Extract a key from:

* a header (`source: header`, `name: X-GitHub-Delivery`)
* a header param (`source: header_param`)
* a JSON path in the body
* the raw body SHA-256 (`source: body_sha256`)

Duplicate keys within the retention window are acknowledged without re-running actions.

## Example: GitHub PR preview

Per-PR sandbox with [lifecycle policy](/concepts/sandbox-lifecycle), GitHub HMAC auth, and delivery to a dev server on port 3000:

```python
import os
from islo import Islo

client = Islo()

webhook = client.webhooks.create_incoming_webhook(
    name="github-pr-preview",
    status="active",
    auth={
        "auth_type": "hmac",
        "algorithm": "sha256",
        "encoding": "hex",
        "prefix": "sha256=",
        "secret": {
            "name": "github_webhook_secret",
            "value": os.environ["GITHUB_WEBHOOK_SECRET"],
        },
        "signature": {"source": "header", "name": "X-Hub-Signature-256"},
        "signed_payload": {"type": "raw_body"},
    },
    target={
        "target_type": "sandbox_name_from_event",
        "source": {"source": "json_path", "path": "$.pull_request.number"},
        "required_prefix": "pr-",
    },
    idempotency={"source": "header", "name": "X-GitHub-Delivery"},
    rules=[
        {
            "when": {"json_path": "$.action", "equals": "opened"},
            "actions": [
                {
                    "action_type": "ensure_sandbox",
                    "template": {
                        "image": "ghcr.io/islo-labs/islo-runner:latest",
                        "vcpus": 2,
                        "memory_mb": 4096,
                        "disk_gb": 20,
                        "lifecycle": {
                            "pause_after_idle": 900,
                            "delete_after": 604800,  # 7 days
                            "auto_resume": "on_activity",
                        },
                    },
                },
                {
                    "action_type": "deliver_to_port",
                    "port": 3000,
                    "auto_resume": "on_activity",
                },
            ],
        },
        {
            "when": {"json_path": "$.action", "equals": "closed"},
            "actions": [{"action_type": "delete_sandbox"}],
        },
    ],
)

print(webhook.receiver_url)  # paste into GitHub → Settings → Webhooks
```

`deliver_to_port` has its own `auto_resume` field, separate from the sandbox `lifecycle.auto_resume`. Set both to `on_activity` when you want webhook delivery to wake a paused sandbox before forwarding.

`payload` on `deliver_to_port` controls what gets forwarded (`original` sends the raw webhook body). Optional `path` appends a path segment on the upstream request.

## Tips

* **PR previews**: combine `ensure_sandbox`, a [lifecycle policy](/concepts/sandbox-lifecycle), and `deliver_to_port` so each PR gets its own sandbox that pauses when idle and deletes on `closed`.
* **Cold starts**: set `auto_resume: on_activity` on both the sandbox lifecycle and `deliver_to_port` so paused sandboxes wake before the payload is forwarded.
* **Deduping**: always configure idempotency for providers that retry (GitHub, Stripe) to avoid duplicate sandbox actions.