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.
A failed delivery is retried up to five times over roughly nine hours, so a brief outage on your side no longer costs the event. 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. Retries cover a failing endpoint, not a paused one: events that occur while paused are lost, not queued, and pausing also stops retries already in flight.

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

Encore retries a failed delivery up to five times over roughly nine hours, with a 5-second timeout per attempt and no ordering guarantee (full table). Three consequences worth designing for:
  1. Commit the credit before you return 2xx. Your 2xx is what tells Encore the reward landed, and it is what stops the retries. Acknowledge on receipt and credit afterwards, and any failure in that later step leaves the user uncredited with no signal to us and no retry. If the credit is genuinely too slow for the 5-second budget, write it to your own durable queue inside the request and treat that write as the commit.
  2. Handle duplicates idempotently. Key on transactionId: record processed ids and make a second delivery for the same id a no-op. A user can have several transactions, so the transaction is the unit, not the user.
  3. Reconcile anything financial. Retries narrow the gap but do not close it: a delivery can still exhaust its attempts if your endpoint is down for the whole window. Do not treat this webhook as your ledger. Reconcile periodically against your Encore dashboard analytics.

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). Send Test is a single attempt and is never retried, unlike a real completion.
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.
Check whether it is still in flight before treating it as lost: a failed attempt is retried at +5 min, +10 min, +30 min, +2 hours and +6 hours, so a delivery can be up to roughly nine hours behind the dashboard. If nothing has arrived after that, every attempt failed (endpoint down, non-2xx, timeout, TLS error) and the delivery was abandoned. Abandoned deliveries are not self-serve replayable, so reconcile from dashboard analytics and contact support if you need the event resent.