Webhooks
Get your sandboxes' events posted to your own HTTPS endpoint, and verify their signature.
pols.so can post your sandboxes’ events to HTTPS URLs of yours as JSON, so you do not have to poll for a sandbox to become ready or to stop.
Adding an endpoint
Owners and admins of your org add and delete endpoints on your account page. An API key cannot: it can only list the endpoints (GET /v1/webhooks) and their deliveries (GET /v1/webhooks/{webhook}/deliveries); see the API reference.
For each endpoint you give:
- an
https://URL of at most 2,048 characters, on a public host, without a user name, password or fragment - the events it gets
- optionally a description of up to 200 characters
When you add it, the page shows its signing secret (whsec_...) once. Copy it then: it is not shown again. An org may have up to 10 endpoints.
Events
| Event | When |
|---|---|
sandbox.ready |
the sandbox’s status became running after a create, resume or fork, so exec and file calls work |
sandbox.stopped |
its status became stopped: after a stop, when the org’s monthly sandbox-hours or credit ran out or a trial org held too much disk, or because its VM stopped by itself (then last_error says so) |
sandbox.error |
its status became error |
sandbox.expired |
its expires_at passed and it is being deleted |
Going to standby and waking from it raise no event.
Deliveries
Each delivery is a POST with a JSON body:
{
"id": "evt_8f2k1m9x0q3z",
"type": "sandbox.ready",
"created_at": "2026-10-04T12:00:00Z",
"org_id": "org_...",
"data": {
"sandbox": {
"id": "sbx_...",
"name": "web",
"status": "running",
"desired_state": "running"
}
}
}
The sandbox also carries last_error and expires_at when it has them. The request has the headers Pols-Signature, Pols-Event-Id, Pols-Event-Type, Pols-Delivery-Id and Pols-Delivery-Attempt (1 for the first attempt).
A delivery succeeds when your endpoint answers with a 2xx status within 10 seconds. Redirects are not followed. A failed delivery is retried with growing waits (30 seconds, then 2, 10 and 30 minutes, then 1, 3, 6 and 12 hours): nine attempts over about a day, after which it is marked failed. Every attempt has the same body and a fresh signature timestamp, so the same event can arrive more than once, and events can arrive out of order.
Events and their deliveries are kept for 30 days.
Verifying the signature
The Pols-Signature header looks like t=1791119032,v1=5f1c...: t is the Unix time of the attempt, v1 the hex HMAC-SHA256, keyed with the endpoint’s secret, of t, a . and the raw body. Before you trust a delivery:
- Compute the HMAC over the raw body as received, before parsing it, and compare it with
v1in constant time. - Refuse timestamps more than 5 minutes from your clock.
- Handle each event
idonce.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const want = createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
const got = Buffer.from(parts.v1 ?? "", "hex");
return got.length === want.length / 2 && timingSafeEqual(got, Buffer.from(want, "hex"));
}