PLATINUM DOCS

Webhooks

Signed HTTP events for sandbox, template, and billing changes.

Webhooks push org events to your endpoint as signed HTTP POST requests. Use them to react to sandbox lifecycle, template builds, and billing alerts without polling. Manage webhooks in the dashboard or with the API.

Configure a webhook

FieldRules
urlRequired. http(s). Must resolve to a public address.
eventsOptional. Defaults to ["*"] (all events).
secretOptional. Your own secret, minimum 16 characters. Generated if omitted.

An org can have a maximum of 20 webhooks. The API rejects the 21st create.

The API rejects URLs that resolve to loopback, link-local, or private addresses with 400. To test a local server, expose it through a public tunnel URL.

Create a webhook

The create response is the only place the signing secret appears. Save secret_returned_once now. No later call returns it.

const wh = await client.webhooks.create({
  url: "https://your.app/hooks/platinum",
  events: ["sandbox.created", "sandbox.deleted"],
});
const secret = wh.secret_returned_once;
wh = client.webhooks.create(
    "https://your.app/hooks/platinum",
    events=["sandbox.created", "sandbox.deleted"],
)
secret = wh["secret_returned_once"]
pt webhook create --url https://your.app/hooks/platinum \
  --events sandbox.created,sandbox.deleted
# REST route: see the API reference (/docs/api)

Choose events

Subscribe with exact event names or ["*"]. The API rejects patterns like "sandbox.*" with 400.

EventFires whendata fields
sandbox.createdPOST /v1/sandboxes succeedssandbox_id, host_id, template, cpu, ram_mb, disk_gb, via ("restore" | "cold-boot")
sandbox.state_updatedA sandbox changes lifecycle statesandbox_id, state, internal_ip
sandbox.deletedA sandbox is deletedsandbox_id, host_id
template.state_updatedAn async template build finishestemplate_id, state (ready | failed), image
org.usage_alertOrg spend crosses a configured alert thresholdspend_micros, alert_micros
org.usage_limit_reachedOrg spend crosses 50 / 80 / 90 / 100 % of its hard limit, once per threshold per periodpct, spend_micros, limit_micros, blocked (true only at ≥100 %)

The ping event comes only from the test endpoint.

Receive a delivery

Each delivery is a signed POST:

POST /hooks/platinum HTTP/1.1
Content-Type:             application/json
x-pt-event:               sandbox.state_updated
x-pt-delivery-webhook-id: wh_01K…
x-pt-signature:           sha256=<hex>

{
  "event":  "sandbox.state_updated",
  "org_id": "org_01K…",
  "ts":     "2026-07-17T16:24:00.000Z",
  "data":   { "sandbox_id": "sbx_01K…", "state": "running", "internal_ip": "10.42.1.7" }
}

Every delivery has this shape. data holds the event-specific fields.

Verify the signature

Compute the HMAC over the raw request bytes, not re-serialized JSON. Re-encoding changes whitespace and breaks the signature.

x-pt-signature is sha256= + hex HMAC-SHA-256(secret, raw_body):

import crypto from "node:crypto";

function verify(sig: string, rawBody: string, secret: string): boolean {
  const expect = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return sig.length === expect.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect));
}
import hmac, hashlib

def verify(sig: str, raw_body: bytes, secret: str) -> bool:
    expect = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expect)

Handle retries

Respond with a 2xx status within 5 seconds. Any other result counts as a failure: non-2xx, timeout, or connection error. Platinum does not follow redirects. A 3xx counts as failed.

Attempt123456
Delayimmediate30 s2 min10 min1 h6 h

After the sixth failure, Platinum marks the delivery dead_letter. Deliveries for a deleted or paused webhook dead-letter at their next retry.

Inspect and retry deliveries

Only failed and dead_letter deliveries are retryable. A manual retry resets the attempt counter and revives dead_letter rows.

await client.webhooks.deliveries(wh.id, { status: "failed" });
await client.webhooks.retryDelivery(wh.id, "whd_01K…");
client.webhooks.deliveries(wh["id"], status="failed")
client.webhooks.retry_delivery(wh["id"], "whd_01K…")
pt webhook deliveries wh_01K… --status failed
pt webhook retry wh_01K… whd_01K…
# REST routes: see the API reference (/docs/api)

deliveries lists each attempt with its status, response_code, and error.

List, update, test, delete

update with enabled: false pauses delivery and keeps the subscription. test sends a synthetic ping event through the real delivery path, with the same signing and retries. Check the webhook deliveries for the result.

await client.webhooks.list();
await client.webhooks.update(wh.id, { enabled: false });
await client.webhooks.test(wh.id);
await client.webhooks.delete(wh.id);
client.webhooks.list()
client.webhooks.update(wh["id"], enabled=False)
client.webhooks.test(wh["id"])
client.webhooks.delete(wh["id"])
pt webhook list
pt webhook update wh_01K… --disable
pt webhook test wh_01K…
pt webhook rm wh_01K…
# REST routes: see the API reference (/docs/api)

Full request and response shapes: API reference.