Webhooks#
Webhooks deliver entry lifecycle events to your systems as they happen. Rather than polling for status, you register an HTTPS endpoint and Evolu POSTs a signed event to it.
Configuring an endpoint#
Webhook delivery is configured per campaign — from the Evolu dashboard, or programmatically via the Webhook Configuration API:
- Endpoint URL — the HTTPS URL that receives events.
- Signing secret — a shared secret used to sign every payload (see below). It is issued once when the configuration is first created.
- Enabled flag — toggle delivery on or off. Set it to disabled to pause webhooks without losing your endpoint or secret.
A campaign with no active webhook configuration simply has its events skipped — no delivery is attempted. The same applies while delivery is disabled.
To point the webhook at a new URL or pause/resume it from your own systems, see the Webhook Configuration endpoint.
Events#
| Event | When |
|---|---|
entry.received | Immediately after an entry is accepted — a delivery confirmation. |
entry.completed | After fiscal validation and fraud checks — the entry is now APPROVED or REJECTED. |
entry.evaluated | After the campaign's mechanics are evaluated — carries the proposed outcomes. |
Full payloads are documented in Webhook Events.
The request#
Each event is delivered as an HTTP POST with a JSON body:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Evolu-Event-Id | Stable unique id of this event — use it to deduplicate. |
X-Evolu-Signature | sha256=<hex> HMAC signature of the raw body. |
User-Agent | Evolu-Webhooks/1.0 |
Verifying the signature#
Every payload is signed with HMAC-SHA256 over the raw request body using your campaign's signing secret. Recompute the signature and compare it, in constant time, against the X-Evolu-Signature header. Reject any request that does not match.
import crypto from 'node:crypto'function verify(rawBody: string, signatureHeader: string, secret: string): boolean { const digest = crypto.createHmac('sha256', secret).update(rawBody).digest('hex') const expected = `sha256=${digest}` return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))}import hmac, hashlibdef verify(raw_body: bytes, signature_header: str, secret: str) -> bool: digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(f"sha256={digest}", signature_header)Always verify
Treat the signing secret as a credential. Reject any webhook whose signature you cannot verify — an unverified request must never be trusted.
Responding#
Return a 2xx status as soon as you have safely received the event. Any non-2xx response, a timeout, or a connection error is treated as a failed delivery.
Do your real work asynchronously — acknowledge fast, process later.
Retries#
Failed deliveries are retried with exponential backoff (≈10s, then ×3 each attempt, capped at one day, up to 10 attempts — roughly a week of coverage). Because retries reuse the same X-Evolu-Event-Id, your handler must be idempotent: deduplicate on that id.
Delivery records#
Every attempt is recorded — status code, response body, duration, and failure reason — so delivery can be inspected and audited from the Evolu dashboard.