Incoming Webhooks

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. 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):

OperationPurpose
CreateRegister a receiver; returns receiver_url
List / GetInspect existing receivers
UpdateReplace auth, target, rules, or status
DeleteSoft-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_typeUse when
fixed_sandbox_nameAll events go to one sandbox (sandbox_name: "staging-receiver")
sandbox_name_from_eventName 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:

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_typeWhat it does
trigger_jobStart a deployed job run (job_name, optional version_id, region, and params mapped from the webhook request)
ensure_sandboxCreate the sandbox from template if it does not exist; no-op if it already exists
resume_sandboxResume a paused sandbox
pause_sandboxPause a running sandbox
delete_sandboxDelete the sandbox
deliver_to_portForward 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.

FieldRequiredNotes
imageyesUse ghcr.io/islo-labs/islo-runner:latest for the platform default
vcpusyesVirtual CPUs
memory_mbyesMemory in MB
disk_gbyesDisk in GB
gateway_profilenoGateway profile for credential injection
workdirnoWorking directory inside the sandbox
snapshot_namenoRestore from a named snapshot on create
initnoTagged object: { "type": "full" }, { "type": "minimal" }, or custom
envnoEnvironment variables
sources / setup_scriptsnoGit clones and setup scripts
lifecyclenolifecycle 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_typeNotes
noneNo verification (use only for testing)
hmacSignature over a signed payload (GitHub, Stripe-style)
header_equals / query_equalsStatic shared secret in a header or query param
basic / bearer_staticHTTP basic or static bearer token
jwtJWKS-based JWT validation
ip_allowlistSource IP restriction
all / anyCombine 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, GitHub HMAC auth, and delivery to a dev server on port 3000:

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, 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.