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#

EventWhen
entry.receivedImmediately after an entry is accepted — a delivery confirmation.
entry.completedAfter fiscal validation and fraud checks — the entry is now APPROVED or REJECTED.
entry.evaluatedAfter 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:

HeaderDescription
Content-Typeapplication/json
X-Evolu-Event-IdStable unique id of this event — use it to deduplicate.
X-Evolu-Signaturesha256=<hex> HMAC signature of the raw body.
User-AgentEvolu-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.