Skip to main content
When one of your users completes an offer and the completion is verified, Encore can POST an offer_completed event to a URL you own. Use it when your server needs to react to a completion: grant a reward, credit a balance, kick off an email. You host one HTTPS endpoint; nothing to poll. This is the final stage of the shared flow, and it fires on verification (the advertiser’s conversion postback), not on the user tapping or claiming; a claim that never converts produces no webhook. The payload contract (headers, body fields, signature spec, delivery guarantees, URL requirements) is the offer_completed webhook reference; this guide is the wiring.
Delivery is fire-and-forget: there are no retries. If your endpoint is down, slow, or returns a non-2xx, that event is dropped and never redelivered. Read Design for the guarantees before you make anything financial depend on it.

Set it up

Configuration lives in the dashboard under Settings for the app you want events for; there is no publishable-key API for webhook config.
1

Enter your webhook URL

Paste the HTTPS URL of your endpoint (for example https://your-server.com/webhooks/encore) and save. One webhook per app: to point somewhere else, edit the existing URL. It must be HTTPS, publicly resolvable, and redirect-free (URL requirements).
2

Copy your signing secret

Creating the webhook generates a whsec_... signing secret. Store it as a server-side secret (an environment variable, never in client code). Rotating it invalidates the old one immediately: update your server first, or you’ll reject deliveries in the gap.
3

Send a test delivery

Send Test POSTs a sample event to your live URL with a real signature from your current secret, exercising your verification path end to end. The sample’s transactionId and userId are not real records, so make sure your handler tolerates an unknown user gracefully instead of throwing.
4

Enable or pause

The status toggle turns delivery on and off. Because there are no retries, events that occur while paused are lost, not queued.

Verify the signature

Never trust an unverified delivery; the signature proves the request came from Encore. Recompute HMAC-SHA256 over <X-Webhook-Timestamp>.<raw_body> with your whsec_... secret and compare hex digests (signature spec).
Sign the exact bytes Encore sent, not a re-serialization. Body parsers can normalize whitespace or escaping in ways that are fatal to a byte-exact HMAC. Capture the raw body: in Express, express.raw() or the verify hook of express.json().
Full header and body contract for the delivery this handler verifies: offer_completed webhook reference.
Three details are easy to get wrong, and all three are security- or correctness-relevant: use the raw body, use crypto.timingSafeEqual rather than ===, and enforce a timestamp freshness window so a captured delivery can’t be replayed at you later.

Design for the guarantees

The guarantees are intentionally thin (delivered once, 5-second timeout, no retries, no ordering, no event id; full table). Three consequences worth designing for:
  1. Return 2xx fast, work asynchronously. Acknowledge as soon as the signature checks out, then hand the work to a queue. Doing database writes or third-party calls before responding is the most common way publishers hit the timeout and lose events they had already received.
  2. Handle duplicates idempotently. Key on transactionId: record processed ids and make a second delivery for the same id a no-op. Without that, a repeat can double-grant a reward.
  3. Reconcile anything financial. A dropped delivery is unrecoverable, so never treat this webhook as your ledger. Reconcile periodically against your Encore dashboard analytics and treat the webhook as a low-latency hint that a completion happened.

Testing locally

The SSRF guard refuses private addresses, so http://localhost:3000 can never be a webhook URL, including for test deliveries. Expose your local server through a tunnel with a public HTTPS hostname (ngrok, Cloudflare Tunnel, Tailscale Funnel), point the webhook at the tunnel URL, and use Send Test. On the demo campaign, a test-conversion call from your own integration simulates the advertiser postback, so you can exercise the full flow without a live advertiser.

Troubleshooting

Almost always a body-bytes problem. Verify against the raw request body, not JSON.stringify(req.body). Also confirm you’re joining timestamp and body with a literal dot, hex-encoding (not base64), using the header timestamp rather than your own clock, and using the current secret if you recently rotated it.
A timeout means your endpoint took longer than 5 seconds: return 2xx as soon as the signature verifies. A connection error means the URL isn’t HTTPS, the host isn’t publicly resolvable (use a tunnel), or the path redirects (redirects are rejected, not followed).
Confirm the status toggle is enabled, and remember the event fires on verified completions, driven by the advertiser, not by claims. Run Send Test first to isolate reachability from event volume.
Expected under the no-retry model: the delivery was attempted, failed (endpoint down, non-2xx, timeout, TLS error), and was dropped. Deliveries are not replayable; reconcile from dashboard analytics.