omg/docs
Sandbox API

Webhooks

Signed lifecycle events the platform emits — wire format, signature verification, and what's self-serve today.

The platform pushes sandbox lifecycle events over HTTP in two forms: platform webhooks (signed, Stripe-style) and per-sandbox agent callbacks. Read this first:

Self-serve subscription is not available yet. There is no API to register your own webhook URL today — receivers are configured platform-side. The wire format below is documented so you can build a receiver ahead of time (and so self-hosted deployments can wire VIBES_WEBHOOK_RECEIVER_URL). Until subscription opens up, poll GET /v1/sandboxes/{id} for state changes.

Platform webhooks

Each event is a POST to the subscriber URL:

POST <your-receiver-url>
Content-Type: application/json
X-Vibes-Webhook-Id: evt_<uuid>
X-Vibes-Webhook-Timestamp: <unix-seconds>
X-Vibes-Webhook-Type: sandbox.snapshot.recorded
X-Vibes-Webhook-Signature: v1=<hex hmac-sha256(timestamp + "." + body)>
{
  "id": "evt_...",
  "type": "sandbox.snapshot.recorded",
  "ownerId": "...",
  "occurredAt": "2026-07-07T13:00:00Z",
  "data": {}
}

data is event-type-specific. Parse the fields you need and ignore extras — producers add fields without notice.

Event types

TypeFires when
sandbox.startedA sandbox booted (cold or fork) and is reachable.
sandbox.stoppedA sandbox transitioned to stopped.
sandbox.snapshot.recordedA snapshot was captured and is durable on its node (rootfs upload may still be running).
sandbox.snapshot.uploadedThe snapshot's rootfs upload finished; it's usable as a fork source from any node.

Verifying the signature

The signature covers timestamp + "." + raw body with HMAC-SHA256, so the timestamp can't be swapped onto a replayed body. Verify against the raw request bytes, not re-serialized JSON:

import { createHmac, timingSafeEqual } from "node:crypto"

function verifyWebhook(secret, headers, rawBody) {
  const ts = headers["x-vibes-webhook-timestamp"]
  const expected = createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex")
  const given = (headers["x-vibes-webhook-signature"] ?? "")
    .split(",")
    .map((p) => p.trim())
    .find((p) => p.startsWith("v1="))
    ?.slice(3)
  if (!given || given.length !== expected.length) return false
  if (!timingSafeEqual(Buffer.from(given, "hex"), Buffer.from(expected, "hex")))
    return false
  // reject stale timestamps (replay window — pick your tolerance)
  return Math.abs(Date.now() / 1000 - Number(ts)) < 300
}

The header may carry multiple comma-separated versions (v1=...,v2=...) during a rotation — verify any one you support.

Delivery semantics

  • Best-effort: 3 attempts per event with backoff of 1s, 5s, 20s (~26s total), then the event is dropped. Treat webhooks as hints and reconcile by polling when you need strict consistency.
  • Any 2xx from your receiver counts as delivered. Respond fast; do real work async.
  • Events can arrive out of order — order by occurredAt, not arrival.

Per-sandbox agent callbacks

Separate mechanism, different scope: sandboxes running the platform's dev-editor agent-server (the process behind dashboard build sandboxes) POST plain JSON events to a per-sandbox callbackUrl:

POST <callbackUrl>
Content-Type: application/json
Authorization: Bearer <callbackToken>

{ "type": "turn_complete", ... }

Event types: agent_server_ready, turn_complete, errors_notified, session_reset, steering_queued, agent_aborted. These are unsigned and fire-and-forget — one attempt, 5-second timeout, no retries.

callbackUrl / callbackToken are set via the agentBootstrap create field, which configures the in-VM agent-server. Raw API-key sandboxes don't run the agent-server (they default skipAppProcesses: true and can't supply the internal agent-server bundle), so these callbacks are effectively internal to the dashboard today. If you create raw sandboxes over the API, nothing will POST to you — poll instead.

What to use today

You wantUse
Know when a sandbox is runningPoll GET /v1/sandboxes/{id} for status: "running"
Know when a snapshot is fork-ready everywherePoll the snapshot response / re-try the fork
Push notifications for lifecycle eventsNot self-serve yet — the format above is what will ship