Outbound webhooks
Get pushed events for deployments, database failovers, and quota changes instead of polling — HMAC-signed, with automatic retries.
PandaStack can push events to your HTTPS endpoints as they happen, so integrations (CI, chat alerts, internal dashboards) don't need to poll.
Register an endpoint
curl -X POST https://api.pandastack.ai/v1/webhooks/endpoints \
-H "Authorization: Bearer $PANDASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/pandastack", "events": ["deployment.succeeded", "deployment.failed"]}'The response includes a whsec_... signing secret — store it; you need it to
verify deliveries. An empty events array subscribes to everything. Up to 10
endpoints per workspace. Internal/private-network URLs are rejected.
Event catalog
| Event | Fires when |
|---|---|
deployment.building | An app deploy starts building |
deployment.succeeded | A deploy went live (blue-green flip done) |
deployment.failed | A deploy failed (payload includes error) |
database.failover.started | A managed-DB failover began |
database.failover.completed | The database is ready on its new host |
quota.warning | A workspace crossed 80% of its included credit |
quota.exhausted | Credit exhausted; resources are being paused |
endpoint.test | You called the test route |
The live list is always at GET /v1/webhooks/events. Sandbox lifecycle events
(create/terminate) are on the roadmap — they live on the agent event bus and
need a control-plane bridge.
Delivery format
{
"id": "wd_1a2b3c...",
"type": "deployment.succeeded",
"created_at": "2026-08-14T10:00:00Z",
"workspace": "acme",
"data": { "deployment_id": "…", "app_id": "…", "status": "succeeded", "git_commit": "…" }
}Headers: X-PandaStack-Event (type), X-PandaStack-Delivery (id), and
X-PandaStack-Signature.
Verify signatures
X-PandaStack-Signature: t=<unix>,v1=<hex> where
v1 = HMAC-SHA256(secret, "<t>." + raw_body). Always verify against the raw
request bytes, and reject stale timestamps (>5 min) to prevent replays:
import hmac, hashlib, time
def verify(secret: str, sig_header: str, body: bytes, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Retries
A delivery counts as successful only on a 2xx response (redirects are
failures — we never follow them). Failures retry with backoff at
1m, 5m, 30m, 2h, 8h (6 attempts total), then the delivery is marked dead.
Inspect recent deliveries with
GET /v1/webhooks/endpoints/{id}/deliveries, and send a test event with
POST /v1/webhooks/endpoints/{id}/test.
Respond 2xx quickly (before doing slow work) — the delivery timeout is 10s.