Skip to main content

Overview

When one of your users completes an offer and the completion is verified on Encore’s side, Encore can POST an offer_completed event to a URL you own. That’s the whole feature: one event type, one URL per app, signed with HMAC-SHA256 so you can prove it came from us. Use it when your server needs to react to a completion — grant an in-app reward, credit a balance, mark a task done, kick off an email. You don’t have to poll anything and you don’t need an SDK; you just host an HTTPS endpoint.
Direction matters — this is Encore → your server.
  • This pageoutbound. Encore calls your endpoint when an offer completes.
  • Webhook Ingestioninbound. You call Encore’s endpoint to forward your own billing’s subscription lifecycle events in for incrementality (NCL) measurement.
They are unrelated flows with different headers, different signing strings, and opposite directions. If you’re forwarding Stripe subscription events into Encore, you want the other page.
Delivery is fire-and-forget: there are no retries. If your endpoint is down, slow, or returns a non-2xx, that event is logged on our side and dropped — it will never be redelivered. Read Delivery guarantees before you make anything financial depend on it.

How it works

  1. A completion is verified. The event fires at the moment Encore marks a transaction verified — driven by the advertiser’s side (a conversion postback, or the advertiser’s own completion call) — not when the user taps or claims the offer. A claim that never converts produces no webhook. (One exception exists for development: on the demo campaign, a test-conversion call from your own integration simulates the postback, so you can exercise the flow end to end without a live advertiser.)
  2. Encore signs and POSTs once. We send a JSON body plus a hex HMAC-SHA256 signature over timestamp.body, keyed by your app’s signing secret.
  3. You verify and acknowledge fast. Check the signature, return 2xx, and do your real work off the request path.

Setting it up

Configuration lives in the dashboard under Settings for the app you want events for. It’s dashboard-only — there’s no publishable-key API for managing webhook config — and everything below is a control on that screen.
1

Enter your webhook URL

Paste the HTTPS URL of the endpoint you want called (for example https://your-server.com/webhooks/encore) and save. One webhook per app — to point somewhere else, edit the existing URL rather than adding a second one.
2

Copy your signing secret

Creating the webhook generates a signing secret of the form whsec_…. Copy it and store it as a secret on your server (an environment variable — never in client code or a repo). You need it to verify every delivery.
3

Send a test delivery

While the webhook is enabled, use Send Test to POST a sample offer_completed event to your URL right now. The dashboard reports back the HTTP status your server returned and how long it took, so you can confirm the endpoint is reachable and your signature check passes before any real traffic arrives. The sample uses realistic placeholder values (see Test deliveries).
4

Enable or pause

The status toggle turns delivery on and off. While paused, no events are sent — and because there are no retries, events that occur while paused are lost, not queued.
You can also rotate the signing secret (invalidates the old one immediately — update your server first, or you’ll reject deliveries in the gap) and delete the webhook entirely.

URL requirements


The request

1772044200 is the unix-seconds form of the 2026-02-25T18:30:00.000Z in the sample body below — the two are generated moments apart on a real delivery, so they will agree to within a second or so. The header value is the one that goes into the signature.

Headers

Body

The usr_… shape above is only a placeholder, taken from the sample Encore sends in a test delivery. Encore does not mint user ids and adds no prefix. In production, userId is byte-for-byte the value your app supplied — an email, a database primary key, a UUID, whatever you happen to use.
That’s the whole body. There is no app id, no campaign id, no creative id, no event id, and no nesting — the payload is flat, exactly the six fields above. If you have a handler written against a nested req.body.data, it won’t work: read the fields off the top level.

Verifying the signature

Never trust an unverified delivery. Anyone can POST JSON at a public URL, so the signature is what proves the request came from Encore. The signature base string is the timestamp and the raw request body, joined by a single dot:
HMAC-SHA256 that string with your whsec_… signing secret and hex-encode it. The result must equal X-Webhook-Signature.
Sign the exact bytes we sent, not a re-serialization. Body parsers and middleware can normalize whitespace, escaping, or encoding in ways that are invisible once the payload is a parsed object but fatal to a byte-exact HMAC — and the guarantee varies by language and parser. Capture the raw body — in Express, express.raw() or the verify hook of express.json().
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.

Delivery guarantees

Be deliberate here — the guarantees are intentionally thin, and designing around them is your job, not ours. Consequences worth designing for:

Return 2xx fast, work asynchronously

Acknowledge as soon as the signature checks out, then hand the work to a queue or a background task. Doing database writes, third-party calls, or email sends before responding is the most common way publishers hit the 5-second timeout and lose events they had already received.

Handle duplicates idempotently

There are no retries, so duplicates are rare — but “rare” is not “never”, and an idempotent handler costs almost nothing. Key on transactionId: record the ids you’ve processed and make a second delivery for the same id a no-op. Without that, a repeat can double-grant a reward.

Reconcile anything financial

Because a dropped delivery is unrecoverable, never treat this webhook as your ledger. If money, credits, or entitlements depend on completions, reconcile periodically against your Encore dashboard analytics and treat the webhook as a low-latency hint that a completion happened, not as the record that it did.

Test deliveries

Send Test in the dashboard posts a sample event to your live URL with a real signature computed from your current signing secret — so it exercises your verification path end to end, not just your routing. The dashboard reports the status code your server returned and the round-trip duration. The sample payload is well-formed and realistic, but its transactionId and userId do not correspond to a real transaction or a real user in your system. Make sure your handler tolerates an unknown user gracefully instead of throwing (which would return a non-2xx and make a working endpoint look broken).

Testing locally

The SSRF guard resolves your hostname before every delivery and refuses private addresses, so http://localhost:3000 and https://192.168.x.x can never be webhook URLs — including for test deliveries. To develop against a local server, expose it through a tunnel that gives you a public HTTPS hostname (ngrok, Cloudflare Tunnel, Tailscale Funnel, or similar), point the webhook at the tunnel URL, and use Send Test. Swap in your production URL when you go live.

Troubleshooting

Almost always a body-bytes problem. Verify against the raw request body, not JSON.stringify(req.body) — re-serializing is not guaranteed to reproduce the bytes we signed. 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.
Your endpoint took longer than 5 seconds. Return 2xx as soon as the signature verifies and move the real work off the request path.
Check that the URL is HTTPS, that the host is publicly resolvable (not localhost, not a private IP — use a tunnel), and that the path doesn’t redirect. Redirects are rejected rather than followed.
Confirm the webhook’s status toggle is enabled, and remember the event fires when a completion is verified — normally driven by the advertiser, not by the user claiming the offer. A claim with no verified conversion produces no webhook. Run Send Test first to isolate reachability from event volume.
Expected under the no-retry model: the delivery was attempted and failed (endpoint down, non-2xx, timeout, TLS error) and was dropped. Deliveries are not replayable. Reconcile from dashboard analytics.

Next steps

  • Offers API — Feed: fetch and render offers in your own UI.
  • Creator Affiliate Links: per-creator trackable links, with a stats endpoint for attribution you can poll.
  • Webhook Ingestion: the inbound direction — forwarding your own subscription events into Encore for incrementality measurement.