Webhooks
A webhook endpoint is an HTTPS URL on a public host plus the event types it wants. A workspace has at most 10 endpoints."secret": "whsec_...". It is shown once. POST /v1/webhooks/{id}/rotate-secret returns a new one, and the old one stops working at once.
Event types
GET /v1/webhooks lists every type with available.
balance.low fires once when free credit drops below the workspace’s low-balance threshold ($2 by default), and again each time sandboxes are stopped because credit ran out. Its data is {reason, balance_micros, threshold_micros, exhausted, sandboxes_stopped?}; see Low-balance events.
The invoice events follow your invoices: invoice.created when an invoice is issued, then invoice.paid or invoice.payment_failed. Their actor is stripe, and data is:
What a delivery looks like
POST to your URL with Content-Type: application/json and these headers:
Zode-Event: the event typeZode-Delivery:whd_..., unique per deliveryZode-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256>
id stays the same across retries and resends, so deduplicate on it.
Verify the signature
v1 is the hex HMAC-SHA256 of <t>.<raw body>, keyed with the whole whsec_... string. Check it against the raw request bytes before parsing, and reject a t more than 5 minutes from now.
Retries, deliveries and tests
- Any 2xx is success. Anything else, a timeout (10 s) or a redirect (not followed) is a failure.
- Failures retry with backoff: 30 s, doubling, capped at 1 hour, with jitter, until 24 hours after the event. Then the delivery is
failed. - An endpoint whose deliveries have all failed for 3 days is turned off (
disabled_reason: "failing").PATCH /v1/webhooks/{id}with{"enabled": true}turns it back on. GET /v1/webhooks/{id}/deliverieslists deliveries newest first, with status (pending,delivering,succeeded,failed), attempts, your response status and the first 2,048 bytes of your response body. Page withlimit(1 to 100) andbefore.POST /v1/webhooks/{id}/deliveries/{delivery_id}/resendqueues the same event and body again with a new delivery id (409endpoint_disabledon a disabled endpoint).POST /v1/webhooks/{id}/testsends onewebhook.testevent now, without retries, and returns the delivery.
OpenTelemetry export
Zode can push each sandbox’s flight recorder data to your OTLP/HTTP collector once a minute. One configuration per workspace.- Data goes to
<endpoint>/v1/metrics,/v1/tracesand/v1/logs. Protocols:http/protobuf(default) andhttp/json. gRPC is not offered. - Up to 20 headers. Their values are stored sealed and never returned;
GET /v1/otelshowsheader_namesonly. - Resource attributes:
service.name=zode-sandbox,zode.org,zode.sandbox,zode.sandbox.name,zode.region,zode.branch,zode.repo. - Metrics:
zode.sandbox.cpu.utilization,zode.sandbox.memory.usage,zode.sandbox.memory.pressure,zode.sandbox.disk.read.iops,zode.sandbox.disk.write.iops,zode.sandbox.network.receiveand.transmit. - Traces: one span per process and one per AI call (
gen_ai.*attributes and cost). Logs: one record per network event,WARNwhen blocked. - Delivery is at least once. Span ids derive from recorder event ids, so your backend can deduplicate. Export starts when you enable it; there is no backfill.
POST /v1/otel/testsends one log record (“Zode connection test”).DELETE /v1/otelremoves the configuration.
Flight recorder
Every sandbox has a flight recorder with four tracks: resource metrics each second, processes, AI calls and network events. Read it per sandbox:fromandtotake unix milliseconds or RFC 3339. The default window is the last hour; one read covers at most 31 days.timelinereturnsentriesin time order and anextvalue; pass it back ascursorfor the next page.limitis 1 to 5,000 (default 500).tracksis any ofmetrics,process,ai,net.- Process rows have
source: "guest": the VM reports them, so treat them as untrusted. The other tracks come from the host. statssummarises the window: samples, average and peak CPU, peak RAM and memory pressure, network bytes, processes, OOM kills, blocked network events, AI calls and AI cost.
The hosts do not write to the flight recorder yet. Today
timeline returns no entries, stats returns zeros, and OpenTelemetry export has nothing to send.