On Runtime, you learn about a sandbox that stopped, failed to start, or could not wake immediately, as it happens. You do not need to run a polling loop. Thirteen event types cover sandboxes, snapshots and volumes. Runtime signs every delivery with HMAC-SHA256. If your endpoint is down, Runtime retries ten times over about three days. Each delivery is 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>If you leave out events, you receive every type. A webhook belongs to the
account. Every key and every owner sees the same webhooks. Owners can manage
them on the Webhooks page. An
account can have 16.
The events
| Type | Sent when |
|---|---|
sandbox.created |
A sandbox is created and starting |
sandbox.running |
It is ready |
sandbox.paused |
It paused. Its memory and files are kept. |
sandbox.woken |
A paused sandbox runs again |
sandbox.stopped |
It stopped; stopReason says why |
sandbox.start_failed |
It stopped before it ran. sandbox.stopped follows. |
sandbox.wake_failed |
A wake did not complete |
sandbox.host_unreachable |
Its server stopped responding |
sandbox.host_reachable |
Its server responds again |
sandbox.recovered |
Its server was lost, and the sandbox is back |
sandbox.recovery_failed |
Its server was lost, and the sandbox cannot come back |
sandbox.secret_not_applied |
A secret's placeholder was sent unreplaced, with the reason |
sandbox.network.limited |
It reached an outbound connection limit, and which limit |
snapshot.ready, snapshot.failed, snapshot.deleted |
A snapshot's lifecycle |
volume.ready, volume.failed, volume.deleted, volume.recovered |
A volume's lifecycle |
egress_secret.created, egress_secret.updated, egress_secret.deleted |
An egress secret was added, replaced or deleted |
upstream_proxy.set, upstream_proxy.removed |
Your upstream proxy was set or removed |
credit.low, credit.empty |
Credit lasts less than 5 days, or none is left |
job.run.failed, job.recovered |
A job run failed, or a later run succeeded |
agent.call.held |
An agent trigger held a call for you, with the reason |
Each body is the event, with the resource as it was at the time of the event.
It has the id, type and createdAt fields, and data.sandbox (or
data.snapshot, data.volume). That object includes the state, previous
state, labels, size, region and funding. Labels you set when you create a
resource come back here. A handler can use them to route an event to the job
that owns the sandbox.
Write the endpoint
Check the signature, act on the event, and return 204 quickly. Put heavy work in a queue, and do it after the response:
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 });}For how the signature check works, and how to check it without the SDK, see 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 for 14 days. Includes each attempt, your endpoint's status, and the 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> --disableThe same actions are available in code: runtime.webhooks.deliveries(id) and
runtime.webhooks.retry(deliveryId). While deliveries fail, the webhook shows
failingSince. 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 the body as text first, verify the signature, then use the data.
- Doing slow work before responding. A response after 10 seconds counts as a failure, and Runtime sends the event again. Respond first, then do the 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 with a private address is refused when you create the webhook. 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 for one sandbox.
Related
- Read sandbox metrics for CPU and memory over time.
- Pause and resume a sandbox, whose
sandbox.pausedandsandbox.wokenevents are sent 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 100 free hours of a 1 GB machine in its first 30 days, with no card.
Facts on this page were checked on 26 October 2026.