Coppice exposes the E2B lifecycle-events read API and lifecycle webhook management surface for sandbox creation, pause, resume, updates, snapshots, forks, and kills. Event history, webhook registrations, and delivery attempts are process-local rings: useful for SDK parity, operator UI, and smoke receipts, but not a durable audit database.
API shape
The gateway serves the same read paths E2B documents:
| Method | Path | Notes |
|---|---|---|
| GET | /events/sandboxes | recent events across the gateway |
| GET | /events/sandboxes/:id | recent events for one sandbox |
| GET | /events/webhooks | list registered lifecycle webhooks |
| POST | /events/webhooks | register a lifecycle webhook |
| GET | /events/webhooks/:id | inspect one webhook |
| PATCH | /events/webhooks/:id | update URL, events, enabled state, or signing secret |
| DELETE | /events/webhooks/:id | unregister a webhook |
| GET | /events/webhooks/deliveries | recent delivery attempts across all webhooks |
| GET | /events/webhooks/:id/deliveries | recent delivery attempts for one webhook |
Supported query parameters:
limit: default 10, clamped to 1..100.offset: default 0.orderAsc: default false, so newest events come first.types: repeated or comma-separated event type filter.
Event records use E2B’s field names:
{
"version": "v1",
"id": "597f6f48-6a45-4b66-a494-d4ea3e87aeaa",
"type": "sandbox.lifecycle.paused",
"eventData": null,
"sandboxBuildId": "python",
"sandboxExecutionId": "8b9ef69bf2354692b19a31d4ecde60c3",
"sandboxId": "8b9ef69bf2354692b19a31d4ecde60c3",
"sandboxTeamId": "gateway-client-id",
"sandboxTemplateId": "python",
"timestamp": "2026-04-26T18:41:15Z"
}
sandboxBuildId, sandboxExecutionId, and sandboxTeamId are local
equivalents because a single-node Coppice gateway does not have E2B’s
hosted team/build control plane.
Emission points
The event producer is deliberately colocated with the lifecycle mutation sites:
sandbox.lifecycle.createdfrom create and snapshot fork.sandbox.lifecycle.pausedfrom manual pause and idle timeout pause.sandbox.lifecycle.resumedfrom manual resume, connect, and AutoResume.sandbox.lifecycle.updatedfrom timeout, refresh, limits, and network policy changes.sandbox.lifecycle.snapshottedfrom durable ZFS snapshot capture.sandbox.lifecycle.killedfrom the shared kill/reaper teardown path.
This means the event stream tracks what the gateway actually did, not what a sidecar guessed by scraping logs.
Webhook delivery
Webhook registration accepts the same core fields E2B documents:
{
"name": "operator sink",
"url": "https://example.invalid/coppice-events",
"enabled": true,
"events": ["sandbox.lifecycle.created", "sandbox.lifecycle.killed"],
"signatureSecret": "secret-for-event-signature-verification"
}
For each matching event the gateway POSTs the event JSON to the target URL with E2B-compatible headers:
e2b-webhook-ide2b-delivery-ide2b-signature-version: v1e2b-signature
The signature matches E2B’s documented verifier shape:
base64(sha256(signatureSecret + rawPayload)) with trailing =
padding stripped. Failed deliveries retry three times with short
backoff, and each attempt is recorded in
/events/webhooks/:id/deliveries so the admin dashboard can show
successful and failed delivery churn.
Receipt
Run on honor:
mise run lifecycle:events-honor
mise run lifecycle:webhooks-honor
The smoke creates a fresh jail sandbox, pauses it, resumes it, updates
its timeout, kills it, then queries events after teardown. It also proves
the repeated types= filter:
GET /events/sandboxes?types=sandbox.lifecycle.paused&types=sandbox.lifecycle.killed
Latest receipt:
benchmarks/results/lifecycle-events/latest.txt
benchmarks/results/lifecycle-webhooks/latest.txt
Remaining work
The parity shape is closed for the single-node gateway. Remaining
production hardening is persistence: store webhook registrations and
delivery attempts under /var/lib/coppice, add operator-triggered
replay, and age out old delivery rows by policy.