# How to receive webhooks for sandbox events Call `runtime.webhooks.create({ url, events })`. Runtime sends each event as a POST request to your HTTPS URL. Each request is signed. Runtime retries for about three days. **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](/docs/observability#webhooks)). ## Create a webhook ```ts check import { 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 now const answer = await runtime.webhooks.test(hook.id); // a signed webhook.test, sent now console.log(answer.lastStatus); ``` ```python check from withruntime import Runtime runtime = 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 once print(runtime.webhooks.test(hook["id"])) ``` ```bash check npx withruntime webhooks create https://example.com/hooks/runtime --events sandbox.stopped,sandbox.start_failed npx withruntime webhooks test ``` 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](https://withruntime.com/account/webhooks). 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: ```ts check import { verifyWebhook } from "withruntime"; const seen = new Set(); // use your database in production export 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](/how-to/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 | ```bash check npx withruntime webhooks deliveries npx withruntime webhooks retry npx withruntime webhooks update --disable ``` The 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` (or `runtime.events.list()`) lists every lifecycle event, newest first, for the account or for one sandbox. ## Related - [Read sandbox metrics](/how-to/read-sandbox-metrics) for CPU and memory over time. - [Pause and resume a sandbox](/how-to/pause-and-resume-a-sandbox), whose `sandbox.paused` and `sandbox.woken` events are sent here. - [OpenTelemetry export](/docs/observability#opentelemetry-export) sends the same events to Grafana, Datadog or Honeycomb as logs. ## Start ```bash no-run npx withruntime webhooks create https://example.com/hooks/runtime ``` The 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.