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.

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. Rotation does not immediately invalidate deliveries already pinned to the retired secret version. Keep the previous and new signing secrets during the overlap and verify each raw body against either; new replays may also retain the older pinned version, and no fixed drain time is implied.

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