01Documentation
Webhooks
Receive real-time event notifications from sandboxes via webhooks. Webhooks allow you to react to process events, sandbox lifecycle changes, and file modifications.
Webhook Configuration#
Configure webhooks when claiming a sandbox:
| Field | Type | Description |
|---|---|---|
url | string | Webhook endpoint URL (required) |
secret | string | Shared secret for payload signing (optional) |
watch_dir | string | Directory to monitor for file events (optional) |
Webhooks are configured per-sandbox at creation time. Each sandbox can have its own webhook configuration.
secret is optional. For quick debugging, you can leave it empty and use services like webhook.site to inspect incoming events directly.
Delivery Guarantees#
Webhook requests may be delivered more than once, so consumers must deduplicate by event_id. Your endpoint should return a 2xx status only after it has durably recorded the event. Retries are best-effort and do not guarantee that an unavailable endpoint eventually receives every event.
When a sandbox is created with a webhook, procd writes signed delivery records to a reserved outbox under /var/lib/sandbox0/procd/webhook-outbox before acknowledging the internal publish request, then dispatches them with backoff. The outbox is part of the persistent sandbox rootfs and remains scoped to the sandbox identity.
Sandbox deletion has an additional manager-level path. When manager commits a sandbox deletion, it writes the exact signed sandbox.deleted request to a PostgreSQL outbox in the same transaction. Sandbox and Pod cleanup never wait for the external endpoint. Manager retries transport errors and HTTP 408, 425, 429, and 5xx responses with exponential backoff for up to 24 hours. Other non-2xx responses are terminal. After the 24-hour delivery window expires, manager stops retrying and later removes the bounded outbox record.
Webhook delivery is eventually consistent. Temporary endpoint outages, procd container restarts, and manager restarts can cause retries. A response can be delivered more than once when the receiver accepts it but manager cannot durably record the success.
Event Types#
Sandbox Events#
| Event | Description |
|---|---|
sandbox.ready | Sandbox has been initialized and is ready |
sandbox.paused | Sandbox has been paused |
sandbox.resumed | Sandbox has been resumed |
sandbox.killed | Sandbox has been killed |
sandbox.deleted | Manager has committed sandbox deletion and queued the final notification |
Process Events#
| Event | Description |
|---|---|
process.started | A new process/context has started |
process.exited | A process has exited (includes exit code) |
process.crashed | A process has crashed unexpectedly |
File Events#
| Event | Description |
|---|---|
file.modified | A file event occurred (create, write, remove, rename, or chmod) |
File events require setting watch_dir in the webhook configuration. The specific operation type is included in the payload's event_type field.
Agent Events#
| Event | Description |
|---|---|
agent.event | Custom event published by the sandbox via the webhook publish API |
Configure Webhook at Sandbox Creation#
Set up webhooks when claiming a sandbox.
gowebhookURL := os.Getenv("SANDBOX0_WEBHOOK_URL") webhookSecret := os.Getenv("SANDBOX0_WEBHOOK_SECRET") sandbox, err := client.ClaimSandbox( ctx, "default", sandbox0.WithSandboxHardTTL(300), sandbox0.WithSandboxWebhook(webhookURL, webhookSecret), sandbox0.WithSandboxWebhookWatchDir("/tmp/webhook-demo"), ) if err != nil { log.Fatal(err) } fmt.Printf("Sandbox ID: %s\n", sandbox.ID)
Signature Verification#
When a secret is configured, webhook payloads are signed using HMAC-SHA256.
If secret is not configured, Sandbox0 will not send the X-Sandbox0-Signature header.
Header#
X-Sandbox0-Signature: <hex-encoded-signature>The signature is the hex-encoded HMAC-SHA256 of the request body.
Verification Example#
goimport ( "io" "net/http" "os" sandbox0 "github.com/sandbox0-ai/sdk-go" ) func webhookHandler(w http.ResponseWriter, r *http.Request) { // Verify against raw request bytes, not a re-serialized JSON object. payload, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "read body failed", http.StatusBadRequest) return } signature := r.Header.Get("X-Sandbox0-Signature") secret := os.Getenv("SANDBOX0_WEBHOOK_SECRET") if !sandbox0.VerifyWebhookSignature(secret, payload, signature) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // Process webhook... w.WriteHeader(http.StatusOK) }
Triggering Webhook Events#
Sandbox Events#
sandbox.ready is emitted after initialization. sandbox.paused and sandbox.resumed are emitted when pause and resume requests complete. sandbox.killed is emitted by procd during graceful shutdown, and sandbox.deleted is durably queued by manager when sandbox deletion commits.
go// sandbox.ready is triggered automatically when the sandbox initializes // Trigger sandbox.paused event _, err = client.PauseSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } log.Println("Sandbox paused - webhook triggered") // Trigger sandbox.resumed event _, err = client.ResumeSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } log.Println("Sandbox resumed - webhook triggered")
Process Events#
go// Trigger process.started and process.exited events _, err = sandbox.Run(ctx, "bash", `echo "hello world"`) if err != nil { log.Fatal(err) } log.Println("Process events triggered") // Trigger process.exited with non-zero exit code _, err = sandbox.Cmd(ctx, `/bin/sh -c "exit 2"`) if err != nil { log.Printf("Command failed (expected): %v", err) } log.Println("Process exited with error code")
File Events#
go// Ensure watch directory exists _, err = sandbox.Mkdir(ctx, "/tmp/webhook-demo", true) if err != nil { log.Fatal(err) } // Trigger file.modified events (with payload.event_type: "create", "write") _, err = sandbox.WriteFile(ctx, "/tmp/webhook-demo/file.txt", []byte("hello")) if err != nil { log.Fatal(err) } log.Println("File created/written - webhook triggered") // Trigger file.modified event (with payload.event_type: "rename") _, err = sandbox.MoveFile(ctx, "/tmp/webhook-demo/file.txt", "/tmp/webhook-demo/file-renamed.txt") if err != nil { log.Fatal(err) } log.Println("File renamed - webhook triggered") // Trigger file.modified event (with payload.event_type: "chmod") _, err = sandbox.Cmd(ctx, `/bin/sh -c "chmod 600 /tmp/webhook-demo/file-renamed.txt"`) if err != nil { log.Fatal(err) } log.Println("File chmodded - webhook triggered") // Trigger file.modified event (with payload.event_type: "remove") _, err = sandbox.DeleteFile(ctx, "/tmp/webhook-demo/file-renamed.txt") if err != nil { log.Fatal(err) } log.Println("File removed - webhook triggered")
Agent Events#
Publish custom events from within the sandbox using the webhook publish API:
go// Publish a custom agent event from within the sandbox _, err = sandbox.Run(ctx, "bash", `curl -X POST http://localhost:49983/api/v1/webhook/publish \ -H "Content-Type: application/json" \ -d '{"payload": {"message": "Task completed", "progress": 100}}'`) if err != nil { log.Fatal(err) } log.Println("Agent event published")