Documentation

Webhook API

Webhooks deliver committed repository changes to an HTTP endpoint outside the mutation request.

Target schema

Each target belongs to one index:

FieldContract
nameUnique target name within the index
urlHTTP or HTTPS endpoint without user info, query, or fragment
secret_envEnvironment variable that contains the HMAC secret
secretLiteral HMAC secret, used instead of secret_env
eventsOptional allowlist of event names exposed by the ecosystem owner

An empty or omitted event list receives each event the owner exposes. Ecosystem guides define event names and payload schemas.

Secret strength

Peryx uses the UTF-8 bytes of the resolved secret or secret_env value as the HMAC-SHA256 key. The value must contain at least 32 bytes. RFC 2104 section 3 recommends a key at least as long as the hash output, which is 32 bytes for SHA-256. Length does not supply entropy; generate each target secret from a cryptographic random source.

Generate 32 random bytes as hexadecimal and create the output with owner-only permissions:

$ umask 077
$ openssl rand -hex 32 > peryx-webhook-secret

Set secret_env to an environment variable containing that file's value, or set secret to the value itself. Peryx checks the resolved value during startup and check-config.

Delivery envelope

Every delivery uses POST with an owner-defined JSON body and these headers:

HeaderContract
Content-Typeapplication/json
User-Agentperyx/<version>
X-Peryx-EventOwner-defined event name
X-Peryx-DeliveryStable delivery identifier across retries
X-Peryx-TimestampUnix second used to sign this attempt
X-Peryx-Signaturesha256=<hex> HMAC-SHA256 signature

Consumers must ignore unknown payload fields so implementations can extend their schemas without breaking receivers.

Signature contract

peryx signs these exact bytes with the target secret:

<timestamp>.<delivery-id>.<raw-json-body>

Receivers compare the HMAC in constant time and reject timestamps outside their replay window. Re-serializing the body before verification changes the signed bytes.

Delivery contract

peryx stores each delivery before a background worker sends it. A process restart retains queued work. A 2xx response completes delivery. Transport failures and HTTP 5xx responses retry with the same delivery ID; 408 and 429 use the same retry path. A valid Retry-After response delays the next attempt when it is later than peryx's local backoff, and the stored deadline survives a process restart. Other 4xx responses are final.

Redirects are final after the first attempt. peryx neither follows nor retries them because sending the signed payload to a target-selected location could move it outside the configured origin. A 302 reports webhook target returned redirect 302; redirects are not followed as its outcome.

Delivery is at least once and does not preserve mutation order. If the receiver accepts a request but peryx loses the process before recording its result, the next process may send the request again. Retries keep the same X-Peryx-Delivery value so receivers can deduplicate them.

Retention

The metadata database holds a row only while a delivery is outstanding: queued, in flight, or waiting on a retry deadline. A delivery that succeeds or exhausts its attempts leaves no row, so the database tracks pending work rather than lifetime event volume.

Each attempt's outcome goes to the peryx::webhook tracing target instead: delivery identifier, index, target name, event name, attempt count, final status, response status, next retry, and bounded error text. It excludes payloads, secrets, signatures, credentials, URL queries, and response bodies. Collect that target to keep delivery history for as long as your operations require.

On this page