How to receive webhooks for sandbox events
Call runtime.webhooks.create({ url, events }); Runtime POSTs each event to your HTTPS URL, signed, retrying for about three days.
On Runtime you hear about a sandbox that stopped, failed to start or could not wake the moment it happens, with no polling loop to run. Thirteen event types cover sandboxes, snapshots and volumes. Every delivery is signed with HMAC-SHA256, retried ten times over about three days when your endpoint is down, and logged with your endpoint's status and timing. Webhooks cost nothing extra (metrics and webhooks).
Create a webhook
TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();const hook = await runtime.webhooks.create({ url: "https://example.com/hooks/runtime", events: ["sandbox.stopped", "sandbox.start_failed", "sandbox.wake_failed"],});console.log(hook.id, hook.secret); // the secret is shown once: store it nowconst answer = await runtime.webhooks.test(hook.id); // a signed webhook.test, sent nowconsole.log(answer.lastStatus);Pythonfrom withruntime import Runtimeruntime = Runtime()hook = runtime.webhooks.create( url="https://example.com/hooks/runtime", events=["sandbox.stopped", "sandbox.start_failed", "sandbox.wake_failed"],)print(hook["id"], hook["secret"]) # shown onceprint(runtime.webhooks.test(hook["id"]))Terminalnpx withruntime webhooks create https://example.com/hooks/runtime --events sandbox.stopped,sandbox.start_failednpx withruntime webhooks test <id>Leave events out to receive every type. A webhook belongs to the account, so
every key and every owner sees the same ones, and owners can manage them on
the Webhooks page. An account can
have 16.
The events
| Type | Sent when |
|---|---|
sandbox.created |
A sandbox is made and starting |
sandbox.running |
It is ready |
sandbox.paused |
It paused, keeping its memory and files |
sandbox.woken |
A paused sandbox runs again |
sandbox.stopped |
It stopped; stopReason says why |
sandbox.start_failed |
It stopped before it ever ran; sandbox.stopped follows |
sandbox.wake_failed |
A wake did not complete |
snapshot.ready, snapshot.failed, snapshot.deleted |
A snapshot's lifecycle |
volume.ready, volume.failed, volume.deleted |
A volume's lifecycle |
Each body is the event with the resource as it was: its id, type,
createdAt and data.sandbox (or data.snapshot, data.volume) with the
state, previous state, labels, size, region and funding. Labels you set at
create come back here, so a handler can route an event to the job that owns
the sandbox.
Write the endpoint
Check the signature, act, and answer 204 quickly. Heavy work belongs in a queue after the answer:
TypeScriptimport { verifyWebhook } from "withruntime";const seen = new Set<string>(); // use your database in productionexport async function POST(request: Request) { let event; try { event = await verifyWebhook( await request.text(), request.headers.get("runtime-signature"), process.env.RUNTIME_WEBHOOK_SECRET!, ); } catch { return new Response("bad signature", { status: 400 }); } const id = request.headers.get("runtime-webhook-id") ?? event.id; if (seen.has(id)) return new Response(null, { status: 204 }); // a retry of one already handled seen.add(id); if (event.type === "sandbox.stopped") { const sandbox = event.data.sandbox as { id: string; stopReason?: string }; console.log("stopped", sandbox.id, sandbox.stopReason); } return new Response(null, { status: 204 });}How the signature check works, and how to do it without the SDK, is on verify a webhook signature.
Answers, retries and the log
| Rule | What happens |
|---|---|
| Success | Any 2xx within 10 seconds |
| Failure | Any other status, a timeout or a redirect |
| Retries | After 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h, 24 h and 24 h: ten attempts in about three days |
| Same event, retried | Same Runtime-Webhook-Id header on every attempt |
| Order | Not guaranteed; order by createdAt |
| Delivery log | Kept 14 days, with attempts, your status and duration |
| Where it may go | HTTPS on the public internet; private, loopback and link-local addresses are refused |
Terminalnpx withruntime webhooks deliveries <id>npx withruntime webhooks retry <deliveryId>npx withruntime webhooks update <id> --disableruntime.webhooks.deliveries(id) and runtime.webhooks.retry(deliveryId) do
the same from code, and the webhook shows failingSince while its deliveries
fail. An agent manages webhooks with the runtime_webhooks_manage MCP tool.
Mistakes and how Runtime handles them
- Parsing JSON before checking the signature. The signature covers the raw body. Read it as text first, verify, then trust it.
- Doing slow work before answering. An answer after 10 seconds counts as a failure and is sent again. Answer first, then work.
- Handling a retry twice. A retry carries the same
Runtime-Webhook-Id; skip one you have already handled. - An endpoint on a private address. A URL naming a private address is refused when the webhook is made, and each delivery resolves the name again and connects only to the public address it checked.
- Missing events while the endpoint was down. Nothing is lost for 14 days:
runtime events(orruntime.events.list()) lists every lifecycle event newest first, for the account or one sandbox.
Related
- Read sandbox metrics for CPU and memory over time.
- Pause and resume a sandbox, whose
sandbox.pausedandsandbox.wokenevents arrive here. - OpenTelemetry export sends the same events to Grafana, Datadog or Honeycomb as logs.
Start
Terminalnpx withruntime webhooks create https://example.com/hooks/runtimeThe first command prints a link to approve in your browser; new accounts get 50 free sandbox hours, no card.
Facts on this page were checked on 25 September 2026.