How to Verify a Referral Install Before You Grant the Reward
Verify a referral install on your server before paying out: check the webhook signature, read the evidence, dedupe by event id, and cap rewards per inviter.
To verify a referral install before you pay a reward, check five things on your server: the webhook signature, the event id you have not seen before, the match evidence, the flags, and your own limits per inviter. Only then grant the credit. WarpLink records the referral and signs the event. Your backend decides.
This guide turns a referral.claimed event into a safe reward, step by step, with code. It assumes the referral link and claim are already working. If they are not, start with Referrals.
Where verification fits
A referral reward has three moving parts. Your app claims the install after sign-up. WarpLink matches it to an invitation link, records it, and sends a signed webhook. Your backend receives the event and grants the reward in your own system.
The first valid claim for an app and installer id wins, so the same installer id cannot be credited twice by us. The id names an install or a device, so one person who reinstalls or signs up on a second device can arrive with a new id. Everything after the event is yours: the amount, the caps, the review queue. That is deliberate. Your rules about what counts stay in your code, and the facts that feed them arrive signed.
Step 1: Claim from your backend
An app can claim by itself with claimReferral, and the record is the same. For rewards of value, send the claim from your server. Your backend holds your rules and your records, so it can claim only after a sign-up passes your own checks.
Pass the attributionId from the SDK's attribution result and the new user's installer id to your server, then call the claims route:
curl -X POST https://api.warplink.app/v1/referrals/claims \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Idempotency-Key: signup-8f21" \
-H "Content-Type: application/json" \
-d '{
"app_id": "YOUR_APP_ID",
"installer_id": "d1c2f9a0-7b43-4f0e-a6d5-3c2b8e1f7a90",
"install_id": "INSTALL_ID_FROM_THE_APP"
}'
The key needs the referrals:write scope. The Idempotency-Key header makes a retry safe: send the same key for the same sign-up and the second request returns the first answer. The installer id is a dedicated random id for the new user, up to 128 characters. It is never an email address or a phone number.
Step 2: Read the claim result
The answer carries a status:
status | What you do |
|---|---|
claimed | Recorded inside the credit window. Wait for the webhook, or grant now by your rules |
flagged | Recorded after the window closed, with the flag outside_window. Your rules decide |
ignored | This installer already holds a credit. The answer names the referral in duplicate_of |
none | No referral link brought this install. Nothing is recorded, and no reward is due |
none is an ordinary answer, not an error. A claim that names the link_id of an ordinary link answers 409 LINK_NOT_REFERRAL and records nothing.
Step 3: Verify the webhook signature
Every delivery is a POST with three headers: X-WarpLink-Signature, X-WarpLink-Timestamp, and X-WarpLink-Event-Id. The signature is a lowercase hex HMAC-SHA256 of ${timestamp}.${rawBody}, keyed with the whole whsec_ secret including its prefix. Verify it before you read a single field:
import crypto from 'node:crypto';
const SECRET = process.env.WARPLINK_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;
export function verify(rawBody, signatureHeader, timestamp) {
if (!/^\d{1,12}$/.test(timestamp)) return false;
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return signatureHeader.split(',').some((signature) => {
const given = Buffer.from(signature.trim(), 'utf8');
const wanted = Buffer.from(expected, 'utf8');
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
});
}
Use the raw body bytes. A framework that parses JSON first hides them, and a re-serialized body does not match. The header can hold two signatures for 24 hours after you rotate the secret, so accept a match on any of them. The complete receiver, with the Express route, is in receive a referral reward webhook in Node.js and the Flask version is in Python.
Step 4: Deduplicate by event id
We deliver each event at least once. A retry, a resend, or a slow answer can put the same event in front of you twice, and the event id is how you tell. Record it before you grant anything, in the same transaction as the reward:
CREATE TABLE webhook_events (id TEXT PRIMARY KEY, received_at TIMESTAMPTZ DEFAULT now());
INSERT INTO webhook_events (id) VALUES ($1) ON CONFLICT DO NOTHING;
-- 0 rows inserted: the event is a repeat, so stop here.
The id is the same on every retry and every resend. An upgrade from an estimate to an exact match arrives as referral.updated with the original id plus _u, so it is a different event and is handled once as well.
Step 5: Read the evidence
The event data tells you how strong the match is:
{
"id": "evt_9d2f41ab",
"type": "referral.claimed",
"data": {
"referral_id": "ref_9d2f41ab",
"link": { "id": "b7a1f2e4-52c8-4a38-8f64-1d7a0c9e3a55", "slug": "invite-ana" },
"installer_id": "d1c2f9a0-7b43-4f0e-a6d5-3c2b8e1f7a90",
"platform": "ios",
"match": { "method": "install_token", "exact": true, "confidence": 1 },
"claimed_via": "sdk",
"flags": []
}
}
match.exact is true for install_token (an invitation pasted on iPhone), android_referrer (the invitation inside the Play referrer), and link_open, all with a confidence of 1. It is false for probabilistic, an estimated iPhone match whose confidence is the matcher's score. A rule that pays full value only for exact matches, and holds estimates for the exact upgrade, is a reasonable default. Exact versus estimated matches defines each field.
Check flags as well. ["outside_window"] arrives on a referral.flagged event, which goes to every endpoint unless you turn it off.
Step 6: Apply your own limits
The signature proves WarpLink sent the event and that nobody changed it on the way. It does not prove the install came from the invitation: an event can carry an estimated match, so read match.exact first (and match.confidence when it is false). Whether the reward is earned is then a business rule, and these are the usual ones to keep in your code:
- A cap per inviter. Count rewards by
link.id, which you stored with the inviting user, and stop at your limit per day or per month. - A minimum action. Grant the reward when the friend finishes a first purchase or a first session, not at install. Hold the event until your own condition is met.
- A review queue for estimates. Send
exact: falsereferrals to a queue and pay whenreferral.updatedupgrades them. - A late-claim policy. Decide whether
referral.flaggedearns the full reward, a smaller one, or none.
async function handle(event) {
const first = await db.insertEventIfNew(event.id);
if (!first) return;
const { installer_id, link, match, flags } = event.data;
const inviter = await db.inviterForLink(link.id);
if (!inviter) return;
if (!match.exact || flags.length > 0) return db.holdForReview(event);
if (await db.rewardsThisMonth(inviter.id) >= MONTHLY_CAP) return;
await db.grantReward(inviter.id, installer_id, event.id);
}
Answer the webhook with a 2xx within 10 seconds and do the slow work from a queue. A 5xx answer or a timeout triggers a retry, up to 5 attempts over 20 minutes.
Step 7: Check the record
Open Referral handoff, then Referrals, in the dashboard. Each row shows the link, the installer id, the match, and the webhook status: claimed, delivered, retrying, failed, flagged, or duplicate. Open a referral to see its claims, the payloads we sent, and every delivery attempt. A failed event can be sent again, with the same id, for 7 days, so an outage on your side does not lose a reward.
Frequently Asked Questions
How do I verify a referral install on the server?
Verify the signature on the referral.claimed webhook, check the event id against the ones you have already handled, read match.exact and the flags, and apply your own rules before granting anything. WarpLink records the referral and sends the event. Your backend decides whether the reward is earned.
Can the same person earn a referral reward twice?
Not for the same installer id. The first valid claim for an app and installer id holds the credit. The id names one install or device, so a person who reinstalls or uses a second device can arrive with a new id. A repeat claim answers ignored and names the referral that holds it, and no second webhook is sent.
What does a flagged referral mean?
A flagged referral was claimed after the credit window closed. We record it with the flag outside_window and send a referral.flagged event, so your rules decide whether a late claim still earns a reward.
Should I claim from the app or from my backend?
For rewards of value, claim from your backend. It holds your rules and your records, so it is the right place to decide when a sign-up has earned the reward. Call POST /v1/referrals/claims with an API key that has referrals:write and an Idempotency-Key header.
What is the installer id?
It is your own random id for the new user, such as a UUID stored in its own column. It is opaque, up to 128 characters, and never an email address, phone number, or device identifier.
Related Guides
- The webhook reference: Receive referral webhooks lists every payload, retry, and limit.
- The claim API: Referrals API and Webhooks API.
- The iPhone install step: Make a referral link survive the App Store install.
- Launch readiness: A referral link checklist for launch.
- Use case: App referral programs.
How WarpLink Helps
We keep the part of a referral that is hard to host yourself: the invitation link, the install match, the record of which tap led to which sign-up, and a signed event that proves it. You keep the part that is yours, which is the reward and the rules around it.
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 next to 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.
See which link drove each install
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.
Related Posts
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.
Receive a Referral Reward Webhook in Python with Flask
Receive a signed WarpLink referral webhook in Python with Flask: verify the HMAC signature on the raw body, dedupe by event id in SQLite, and test locally.
Make a Referral Link Survive the App Store on iPhone
How a referral code survives the App Store install on iPhone: the invitation page, the system paste button, and claimReferral, with SwiftUI code.