Skip to content
pols.so docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

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:

  1. Compute the HMAC over the raw body as received, before parsing it, and compare it with v1 in constant time.
  2. Refuse timestamps more than 5 minutes from your clock.
  3. Handle each event id once.
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"));
}