Skip to main content
guides

How to Receive a Referral Reward Webhook in Node.js

Receive a signed WarpLink referral webhook in Node.js with Express: verify the HMAC signature on the raw body, dedupe by event id, and test locally.

WarpLink Team8 min read

To receive a WarpLink referral webhook in Node.js, read the raw request body, verify the X-WarpLink-Signature HMAC-SHA256 over ${timestamp}.${rawBody} with the whole whsec_ secret, skip event ids you have seen, and grant the reward. This guide gives a complete Express receiver you can run and test locally.

You get a server that verifies every delivery, handles referral.claimed, referral.flagged, and referral.updated, answers inside the 10-second limit, and a script that signs a sample event so you can test without a phone. The same flow in Python is in the Flask version.

What arrives

Every delivery is a POST with Content-Type: application/json and three headers:

HeaderValue
X-WarpLink-SignatureOne or more lowercase hex signatures, separated by commas
X-WarpLink-TimestampThe send time in seconds since the Unix epoch
X-WarpLink-Event-IdThe event id, the same value as id in the body

The body is a JSON object with four fields: id, type, created, and data. For a referral event, data holds referral_id, app_id, link (with id, slug, and url), installer_id, platform, match (method, exact, confidence), tapped_at, installed_at, claimed_at, claimed_via, and flags. The full payload is in the webhooks guide.

The signature is the lowercase hex HMAC-SHA256 of the timestamp, a period, and the raw body. The key is the whole signing secret, whsec_ prefix included. During the 24 hours after a secret rotation the header holds two signatures, current secret first.

Step 1: Create the endpoint and copy the secret

Open the app in the dashboard, go to Referral handoff, choose the Webhook tab, and add an endpoint with a public https address. Copy the signing secret. It starts with whsec_ and is shown in full once. Free plans have 1 endpoint per app and paid plans have 5.

Keep the secret in an environment variable:

Step 2: Set up the project

Node 18 or later runs both files below as written.

Step 3: Write the verifier

The verifier follows the order in the docs: check the timestamp is well formed and within 300 seconds, compute the HMAC over the raw body, and compare in constant time against every listed signature. Save this as verify.js:

Two details matter. crypto.timingSafeEqual throws when the buffers differ in length, so the length check comes first. And the some call accepts a match on any listed signature, which is what makes a secret rotation painless.

Step 4: Write the receiver

Save this as server.js. It reads the body with express.raw so the bytes stay untouched, verifies, stores the event id, and answers fast:

Start it:

Four choices are worth naming:

  • express.raw on this route only. A global express.json() hides the raw bytes the signature covers. Mount it on other routes if you need it, never before this one.
  • Reject with 400, not 401. Any 4xx ends the event as failed, so an invalid signature is never retried. Only a 5xx or a timeout retries.
  • Answer first, work after. We wait 10 seconds for a reply and count a slower answer as a timeout. setImmediate moves the work out of the request. In production, push the event to a queue instead.
  • Dedupe before you grant. Delivery is at least once. The event id is the same on every retry and every resend.

Step 5: Test without a phone

The dashboard's Send test button sends a signed webhook.test event to a public address. For a local server, sign a sample event yourself. Save this as send-test.js:

Run it in a second terminal with the same secret:

You see 204 twice, and the server prints one reward for installer ... line. The second 204 is the dedupe working: same event id, no second reward. Change one character of rawBody after signing and you get 400 invalid signature.

To reach your machine from the dashboard, expose port 3000 through any https tunnel, add that address as the endpoint, and press Send test. The test event has an id that starts evt_test_, goes out once, and is never retried.

Step 6: Move to production

  • Persist the event id. Replace the Set with a table that has the id as its primary key and insert before you grant, in the same transaction as the reward.
  • Use a queue. Acknowledge the delivery and process from the queue so a slow reward system never causes a timeout.
  • Handle referral.updated. It carries previous, the old link and match. Take the credit from the previous link's owner and give it to the new one. See exact versus estimated matches.
  • Rotate with no downtime. A rotation signs with the old and the new secret for 24 hours. The verifier above already accepts either signature, so deploy the new secret at your own pace.
  • Resend after an outage. A failed event can be sent again with the same id for 7 days, from the Referrals report or with POST /v1/webhook-events/{id}/resend.

For the rules that decide whether a claim should be paid, read verify a referral before you grant the reward.

Frequently Asked Questions

Read the raw request body, build the string timestamp, a period, and the body, and compute an HMAC-SHA256 with the whole whsec_ secret as the key. Compare the lowercase hex digest to each signature in the X-WarpLink-Signature header with crypto.timingSafeEqual, after checking the timestamp is within 300 seconds.

Why does my signature check fail with express.json()?

express.json() parses the body and a re-serialized copy no longer matches the bytes we signed. Use express.raw with the application/json type on the webhook route and parse the JSON yourself after the check.

Does the secret include the whsec_ prefix?

Yes. The HMAC key is the full signing secret string, including the whsec_ prefix, read as UTF-8.

How do I avoid granting a reward twice?

Store the event id with the reward and skip an id you have already handled. Delivery is at least once, and the id is the same on every retry and every resend.

What status code should the endpoint return?

Return a 2xx status within 10 seconds. A 5xx answer or a timeout triggers a retry, up to 5 attempts over 20 minutes. A 4xx or 3xx answer ends the event as failed.

We do the matching, keep the record, and send a signed event the moment an install is credited. Your receiver stays small because the hard part, proving which tap led to which sign-up, is already done.

One workflow, three pillars. Deep linking carries the invitation, install attribution credits the link that produced the new user, and real-time analytics shows referrals beside every other link. Sub-10ms redirects on the way in, one SDK on the way out.

Create a free WarpLink account and get deep linking, install attribution, and real-time analytics in one SDK, with 10,000 clicks a month on the free plan.

Try WarpLink on one real link

Deep linking, install attribution, and real-time analytics in one SDK. Free for 10K clicks a month, no sales call.

Stuck on setup? Email support@warplink.app and we will help you get one real link working.

WarpLink Team

Building affordable, reliable link infrastructure for mobile teams. Deep linking, install attribution, and real-time analytics in one SDK.