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, pollGET /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
| Type | Fires when |
|---|---|
sandbox.started | A sandbox booted (cold or fork) and is reachable. |
sandbox.stopped | A sandbox transitioned to stopped. |
sandbox.snapshot.recorded | A snapshot was captured and is durable on its node (rootfs upload may still be running). |
sandbox.snapshot.uploaded | The 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 want | Use |
|---|---|
| Know when a sandbox is running | Poll GET /v1/sandboxes/{id} for status: "running" |
| Know when a snapshot is fork-ready everywhere | Poll the snapshot response / re-try the fork |
| Push notifications for lifecycle events | Not self-serve yet — the format above is what will ship |