event:12345678910{ "type": "sandbox.stopped", "sandboxId": "sb-3f9a…", "eventId": "sb-3f9a…:3:142", "event": { "id": "sb-3f9a…:3:142", "ts": "2026-06-24T12:00:00Z", "orgId": "…", "sandboxId": "sb-3f9a…", "type": "sandbox.stopped", "data": { "reason": "user_requested" } } }
deliveryId — verifyWebhook sets it from the svix-id header (stable across retries and redelivery). Need to route on your own metadata? Attach metadata when you register the destination and it rides as custom HTTP headers on every request — there's no metadata in the body.eventTypes (exact like sandbox.stopped, or a prefix like sandbox.*).| Event | When | event.data |
sandbox.created | the sandbox resource is accepted | template |
sandbox.ready | the VM has booted and can run commands | — |
sandbox.hibernated | the sandbox was paused (disk persisted) | — |
sandbox.resumed | a hibernated sandbox woke | — |
sandbox.stopped | the sandbox stopped | reason: user_requested | expired | crash |
sandbox.migrated | the sandbox moved to new infrastructure | — |
sandbox.checkpoint.created | a checkpoint was taken | checkpointId |
sandbox.forked | a sandbox was forked from a checkpoint | parentId |
sandbox.scaled | the sandbox's resources changed | cpuCount, memoryMB |
sandbox.preview_url.changed | a preview-URL port mapping was added or removed | port, url (url is null when the mapping was removed) |
sandboxId. Set enabled: false to pause a destination — it stops receiving deliveries until you re-enable it; events that occur while it's paused aren't backfilled.12345678910import { Webhooks } from "@opencomputer/sdk"; const webhooks = new Webhooks({ apiKey: process.env.OPENCOMPUTER_API_KEY! }); const { id, secret } = await webhooks.create({ url: "https://app.example.com/oc-webhook", eventTypes: ["sandbox.stopped", "sandbox.ready"], name: "prod", // optional display name }); // `secret` (whsec_…) verifies deliveries — returned here and re-fetchable via webhooks.getSecret(id).
secret, OpenComputer generates one (whsec_…) and returns it on create. The secret is re-fetchable any time via webhooks.getSecret(id) (GET /api/webhooks/:id/secret), and rotatable via webhooks.update(id, { rotateSecret: true }).| Call | Purpose |
webhooks.create(params) | Register a new destination (returns the secret). |
webhooks.list() | List destinations. |
webhooks.get(id) | Fetch one (hasSecret, not the secret value). |
webhooks.getSecret(id) | Fetch the current signing secret (whsec_…). |
webhooks.update(id, params) | Pause/resume (enabled), retune eventTypes, change url, or rotateSecret. |
webhooks.delete(id) | Delete — removes the endpoint; deleted history isn't queryable. |
webhooks.test(id) | Enqueue a sample event (delivered asynchronously). |
sandbox.created and sandbox.ready fire before a separate webhooks.create call could know the sandbox id. To capture a sandbox's full lifecycle, register the webhook with the sandbox — it's pinned to that sandbox and gets every event from created on:123456import { Sandbox } from "@opencomputer/sdk"; const sandbox = await Sandbox.create({ webhooks: [{ url: "https://app.example.com/oc-webhook" }], }); // the generated signing secret is on the create response: sandbox.webhooks[0].secret
| Call | Purpose |
webhooks.deliveries.list(id) | List recent delivery attempts (up to 50). |
webhooks.deliveries.get(id, messageId) | One delivered message, in detail. |
webhooks.deliveries.redeliver(id, messageId) | Re-send a message (same svix-id). |
status and the consumer's responseStatusCode; Svix retries failed attempts on its managed schedule and retains recent attempt history (the attempt index lags a send by a few seconds). A deleted destination's history is no longer queryable.1Delivery = { id, status: "success" | "pending" | "failed", responseStatusCode?, timestamp }
svix-id) — for when the original never landed. A receiver that dedupes on svix-id treats it as the same message.svix-id, svix-timestamp, and svix-signature = v1,<base64(HMAC-SHA256(secret, "{svix-id}.{svix-timestamp}.{rawBody}"))>. Verify against the raw request body, before parsing it; the timestamp guards replay. Any custom headers you attached as destination metadata ride along too. Your signing secret is re-fetchable any time via GET /api/webhooks/{id}/secret and rotatable via update (rotateSecret: true).svix-id (the verifier exposes it as both deliveryId and dedupeId).sandbox.ready before sandbox.created). Treat each event independently and key off event.type; don't assume an ordering.2xx. Failures are retried on Svix's managed backoff (immediately, then 5s, 5m, 30m, 2h, 5h, 10h, 10h); an endpoint that keeps failing for ~5 days is auto-disabled. Inspect attempts and recover failed messages via the deliveries API. Unsafe targets (private / loopback / cloud-metadata addresses) are blocked by the delivery layer.verifyWebhook used for session webhooks; pass SandboxLifecycleEvent to type the nested event:123456789import { verifyWebhook, type SandboxLifecycleEvent } from "@opencomputer/sdk"; // rawBody = the RAW request body string, read BEFORE JSON.parse. // Throws on a missing / expired / invalid signature. const delivery = await verifyWebhook<SandboxLifecycleEvent>(rawBody, request.headers, process.env.OC_WEBHOOK_SECRET!); if (delivery.type === "sandbox.stopped") { console.log(delivery.sandboxId, delivery.event.data.reason); // event.data is typed per event }
svix-id / svix-timestamp / svix-signature headers.verifyWebhook helper. Session webhooks differ in a couple of specifics today: their webhook-id is the event id, and signing is opt-in (set a secret).