Documentation menu

Connect

Webhooks

How to receive Project and Environment events in your own systems.

A webhook subscription tells Perstack to push domain events to a URL you control as they happen. Use it when your own systems need to react to a Project or Environment change without polling. A subscription is created under an explicit Project Environment scope; it does not receive events from another Project.

Setting one up

Creating a subscription takes an HTTPS endpoint URL and the event types it should receive. It is created under an explicit Project Environment scope and does not receive events from another Project or Environment. The subscription metadata reports its lifecycle mode as live, paused, or deleting; these are persisted states, not selectable delivery modes in the create request.

A dedicated installation accepts outbound subscriptions only when delivery is explicitly enabled and the destination is in its installation-owned private HTTPS policy. The policy constrains the origin, port and every resolved address against approved private CIDRs; subscription settings cannot override it. When delivery is disabled or the destination is unavailable, creating or re-enabling a subscription returns 409 Conflict with details.conflictReason outbound_webhooks_unsupported and changes nothing. Listing, reading, pausing and deleting remain available. Ask the installation operator to confirm an existing approved destination; creating a subscription does not grant network access. SaaS delivery keeps its public HTTPS/SSRF policy.

The response to creating a subscription includes a signing secret. It is shown exactly once — store it. The same one-time secret rule applies when you create a replacement secret for an existing webhook. See Signed delivery below.

Full request and response shapes are in the API reference.

Webhook operations use the authenticated principal's grants. Listing and reading require webhooks:list; creation and update require webhooks:create; removal requires webhooks:delete; secret rotation requires webhooks:rotate-secret; dead-letter retry requires webhooks:retry-dead; and retained-delivery replay requires webhooks:replay. A grant does not bypass the explicit Organization, Project, and Environment scope of the request.

Signed delivery

Every delivery body is the canonical event extended with deliveryId, signedAt (an ISO 8601 timestamp), and replay: { generation }. The only signature header is x-perstack-signature, containing lowercase hexadecimal HMAC-SHA256 over the exact raw body. Verify it with a constant-time comparison and reject signedAt values outside a five-minute window.

If a signing secret is compromised, or you simply want to rotate it, rotate the secret on the existing subscription. The replacement is returned once and is not shown again. Once the rotation commits, every later delivery attempt is signed with the new secret, including retries and replays of earlier events. Only an attempt that was already claimed when the rotation committed may still carry the previous secret: at most one attempt per subscription, claimed for up to 60 seconds plus the request itself. Accept both secrets for that short window, then drop the previous one. Rotation is how you revoke a leaked secret.

Retries and recovery

Any HTTP non-2xx response is retryable and redirects are never followed. Network errors and timeouts use bounded exponential backoff with jitter; a valid Retry-After is honored up to one hour. Invalid URL, non-HTTPS, blocked/private-host, and address-validation policy failures are permanent. After eight attempts or the 24-hour delivery bound, the persisted delivery status is dead_letter; retry-dead resets an eligible exact delivery to pending without rewriting event history. An ineligible delivery returns 409 Conflict.

Replaying a retained delivery is different from retrying it. A replay creates a new durable delivery request for the same original event identity while preserving the original delivery's outcome and attempt history. It does not turn an active or completed delivery into a retry, and it requires an Idempotency-Key header containing no spaces and only printable ASCII characters (! through ~), 1–64 characters. Reuse the same key for a network retry; a changed request or an in-progress or mismatched claim is a 409 Conflict and must not be treated as a new delivery.

Because retries and replays can result in the same event being delivered more than once, an endpoint that acts on events should treat delivery as at-least-once and be able to handle receiving the same event twice. Replays are limited to retained deliveries; an unavailable parent, delivery, or cross-scope resource is not enumerated as an empty success.

See the API reference for the specific operations these recovery paths correspond to.

Related