Skip to main content
Preview — sandbox webhooks are newly available and still evolving; the shape may change. We’d love your feedback.
Register a destination and OpenComputer delivers a sandbox’s lifecycle events to it as a small, signed delivery envelope — signed, retried, and redeliverable. Use it to react when a sandbox boots, hibernates, stops, or scales, without polling. The envelope is camelCase, with top-level routing fields and the full event nested under event:
{
  "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" }
  }
}
Dedupe on deliveryIdverifyWebhook 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.

Events

Subscribe to all events, or filter with eventTypes (exact like sandbox.stopped, or a prefix like sandbox.*).
EventWhenevent.data
sandbox.createdthe sandbox resource is acceptedtemplate
sandbox.readythe VM has booted and can run commands
sandbox.hibernatedthe sandbox was paused (disk persisted)
sandbox.resumeda hibernated sandbox woke
sandbox.stoppedthe sandbox stoppedreason: user_requested | expired | crash
sandbox.migratedthe sandbox moved to new infrastructure
sandbox.checkpoint.createda checkpoint was takencheckpointId
sandbox.forkeda sandbox was forked from a checkpointparentId
sandbox.scaledthe sandbox’s resources changedcpuCount, memoryMB
sandbox.preview_url.changeda preview-URL port mapping was added or removedport, url (url is null when the mapping was removed)

Destinations

A destination is org-scoped by default (all your sandboxes), or pinned to one sandbox with 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.
import { 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).
Every destination is signed. If you don’t pass a 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 }).
CallPurpose
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).

Subscribe at create

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:
import { 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

Deliveries

Every send and its outcome — the data behind a deliveries dashboard.
CallPurpose
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).
Each record is a Svix delivery attempt with its 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.
Delivery = { id, status: "success" | "pending" | "failed", responseStatusCode?, timestamp }
Redelivery re-sends the same message to the endpoint (same svix-id) — for when the original never landed. A receiver that dedupes on svix-id treats it as the same message.

Signing, retries, and dedupe

  • HTTPS only, checked at registration. The delivery layer (Svix) additionally blocks private, loopback, link-local, and cloud-metadata addresses at send time.
  • Always signed with Standard Webhooks (delivery is by Svix). Every request carries 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).
  • At-least-once. Once an event is handed off it’s retried until it succeeds or the delivery layer gives up. A duplicate can still arrive, so dedupe on svix-id (the verifier exposes it as both deliveryId and dedupeId).
  • Not strictly ordered. Events for one sandbox can arrive out of order (e.g. sandbox.ready before sandbox.created). Treat each event independently and key off event.type; don’t assume an ordering.
  • Success / retry / give-up. A delivery succeeds on HTTP 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.

Verify a webhook

The SDK ships a verifier — no extra dependency, runs in Node / Cloudflare Workers / browsers — that checks the signature and returns the parsed envelope. It’s the same verifyWebhook used for session webhooks; pass SandboxLifecycleEvent to type the nested event:
import { 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
}
Or verify with any Standard Webhooks / Svix library directly, against the raw body and the svix-id / svix-timestamp / svix-signature headers.
Webhooks here cover sandbox lifecycle. To deliver an agent’s session events (turns, messages, results), see Agent Sessions webhooks — verified with the same 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).