Referrals
Credit an install to the invitation link that brought it. Set up the iPhone paste step, claim from your app or backend, and receive a signed webhook.
A referral link is an invitation one of your users shares with a friend. When the friend installs and signs up, we credit the install to the link, tell your backend with a signed webhook, and you grant the reward in your own system. WarpLink does the matching and keeps the record. The reward, the rules around it, and the money stay with you.
This guide walks the whole path: link, install, claim, webhook, report.
How a referral works
- You create a referral link. It is an ordinary WarpLink link with referral turned on.
- A friend taps it. An iPhone visitor without your app sees a short invitation page that copies an invitation, then goes to the App Store. An Android visitor goes to Google Play with the invitation inside the install referrer.
- The app opens for the first time. On Android the SDK reads the invitation by itself. On iPhone the person taps a paste button and the SDK reads the invitation they paste.
- The friend signs up. Your app calls
claimReferralwith your own id for the new user, or your backend calls the claims API. - We record the referral and send
referral.claimed. Your backend grants the reward.
Before you start
- Register the app with its App Store URL for iOS, and its package name and signing fingerprints for Android. Find your app identifiers shows where each value lives.
- Update the SDK to 1.2.0 or later. Builds on 1.1.x keep working for links and installs, but they cannot read an invitation.
- Open the app in the dashboard, go to Referral handoff, and choose Setup. The checks show what each platform still needs. When iOS is ready, turn on Offer referral handoff on iOS.
- Choose the Referral credit window: 24 hours, 7 days (the default), or 30 days from the tap. A claim inside the window is credited. A later claim is recorded and flagged. A change applies to taps after you save it.
Turning on the iOS handoff needs an App Store URL and an iOS build on SDK 1.2.0 or later seen in the last 30 days. Without them the request answers 409 RECOVERY_PREREQUISITES_MISSING, and details.missing lists app_store_url, sdk_version, or both. Android needs no switch, only a signing fingerprint on the app. The Setup Check ids ios_recovery_store_url, ios_recovery_sdk and android_recovery_fingerprint report each requirement, and referral_readiness on GET /v1/apps/{id} returns the same answer.
While the iOS handoff is off, an iPhone referral can still be credited as an estimated match from device signals, and a later exact claim from the same person upgrades it. See Exact and estimated matches.
Create referral links
Any link that belongs to an app can be a referral link. A link with no app cannot, and the request answers 400 VALIDATION_ERROR.
In the dashboard: open a link and turn on Referral link. In the links list, select links and choose Make referral links to convert many at once, and filter the list to referral links only.
Over REST: send is_referral when you create a link, or change it later with a PATCH. The key needs links:write.
curl -X POST https://api.warplink.app/v1/links \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"app_id": "YOUR_APP_ID",
"slug": "invite-ana",
"destination_url": "https://example.com/invite",
"is_referral": true
}'
Convert an existing link the same way:
curl -X PATCH https://api.warplink.app/v1/links/LINK_ID \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "is_referral": true }'
The link returns is_referral and referral_since, the time it became a referral link. A tap before that time is not a referral. GET /v1/links?is_referral=true lists only referral links.
Over MCP: create_link, update_link and list_links take is_referral, so an assistant can mint an invitation link for each new user. The AI access page lists the referral tools.
Create the link when your user asks to invite someone, and store the link id with that user. When the webhook arrives, link.id tells you whose invitation earned the credit.
The invitation page
An iPhone visitor without your app lands on a short page before the App Store. Set its look and wording under Referral handoff, then Page. The same page is available as GET and PUT /v1/apps/{id}/referral-page and through the MCP tools get_referral_page and update_referral_page. Ordinary links never show it.
The paste step on iPhone
On iPhone the install cannot carry the invitation, so the first launch asks for it. Put the paste button on the screen where a new person first sees your app.
import SwiftUI
import WarpLink
struct WelcomeView: View {
@State private var message: String?
var body: some View {
VStack(spacing: 16) {
Text("Did a friend invite you?")
WarpLinkPasteButton { result in
switch result {
case .success(let link):
// nil means nothing was confirmed.
if link != nil { message = "Invitation applied." }
case .failure(let error):
// The message says how to open the invitation again.
message = error.localizedDescription
}
}
if let message { Text(message) }
Button("Skip") { continueOnboarding() }
}
}
}import UIKit
import WarpLink
final class WelcomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let paste = WarpLinkUIPasteButton { [weak self] result in
switch result {
case .success(let link):
if link != nil { self?.showApplied() }
case .failure(let error):
self?.showMessage(error.localizedDescription)
}
}
view.addSubview(paste)
}
}import { WarpLink } from '@warplink/react-native';
// The person pastes into a field on your onboarding screen.
async function onInvitePasted(text: string) {
try {
const link = await WarpLink.redeemInvitation(text);
if (link) showApplied(); // null means nothing was confirmed
} catch (error) {
// The message says how to open the invitation again.
showMessage((error as Error).message);
}
}import 'package:warplink_flutter/warplink_flutter.dart';
// The person pastes into a field on your onboarding screen.
Future<void> onInvitePasted(String pastedText) async {
try {
final link = await WarpLink.redeemInvitation(pastedText);
if (link != null) showApplied(); // null means nothing was confirmed
} on WarpLinkServerException catch (error) {
// The message says how to open the invitation again.
showMessage(error.message);
}
}The SDK never reads the clipboard. The system paste button asks the person to allow each paste, and the SDK sees only the text that button hands over. React Native and Flutter take the pasted text from a field you provide.
Keep onboarding open
The paste step is an offer, not a gate. A person can skip it, the copy can fail, and the clipboard can hold something else by the time they paste. In each case redeemInvitation returns no match and onboarding continues, and the paste step stays available on the screen if the person comes back to it. Show a referral as applied only when you get a link back. Without one, nothing is confirmed, so do not tell the person a referral was applied.
Expired or invalid invitations
An invitation that has expired, was changed, or belongs to another app is refused with 400. The error message tells the person to open the invitation link again or ask the friend for a new one, and your app can show it as it is. An invitation also expires when its link is paused or ended. See RECOVERY_TOKEN_EXPIRED and RECOVERY_TOKEN_INVALID on the errors page.
When the server has not yet received a just-issued invitation, it answers 503 RECOVERY_TOKEN_PENDING with a Retry-After header. The SDK keeps the invitation on the device and tries again on later launches.
In React Native and Flutter, an invitation delivered on a later launch reaches your app through getAttributionResult() and claimReferral, not through the onLink callback. Call claimReferral after sign-up as usual and it picks the invitation up.
Android
There is nothing to paste. Google Play hands the install referrer to the SDK at first launch, the invitation rides inside it, and the SDK matches it with no screen of yours. Register the app's signing fingerprint so the Setup Check can confirm the path.
Claim the install
After the friend signs up, tell WarpLink which of your users they are. You send an installer id: your own id for the new user.
Make the installer id a dedicated random id, such as a UUID stored in its own column on your users table. The rules:
- It is opaque, up to 128 characters, and one per person.
- It is a separate id, not your primary key, an email address, a phone number, or any other personal data.
- We store it as written and do not check what it looks like.
The first valid claim for an app and installer id wins. Sending the same claim again is safe.
WarpLink.claimReferral(installerId: user.warplinkId) { result in
guard case .success(let claim) = result else { return }
switch claim.status {
case .claimed, .flagged:
break // recorded. Your webhook grants the reward.
case .ignored:
break // already credited: see claim.duplicateOf
case .none:
break // no referral link brought this install
}
}WarpLink.claimReferral(user.warplinkId) { result ->
result.onSuccess { claim ->
when (claim.status) {
ReferralClaimStatus.CLAIMED, ReferralClaimStatus.FLAGGED -> Unit // recorded
ReferralClaimStatus.IGNORED -> Unit // already credited: see claim.duplicateOf
ReferralClaimStatus.NONE -> Unit // no referral link brought this install
}
}
}const claim = await WarpLink.claimReferral(user.warplinkId);
switch (claim.status) {
case 'claimed':
case 'flagged':
break; // recorded. Your webhook grants the reward.
case 'ignored':
break; // already credited: see claim.duplicateOf
case 'none':
break; // no referral link brought this install
}final claim = await WarpLink.claimReferral(user.warplinkId);
switch (claim.status) {
case ReferralClaimStatus.claimed:
case ReferralClaimStatus.flagged:
break; // recorded. Your webhook grants the reward.
case ReferralClaimStatus.ignored:
break; // already credited: see claim.duplicateOf
case ReferralClaimStatus.none:
break; // no referral link brought this install
}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"
}'A none result is an ordinary answer, not an error. The call fails only when the server cannot be reached, the key is refused, or the installer id is empty or longer than 128 characters.
Claim results
status | Meaning |
|---|---|
claimed | The install is credited to the referral link, inside the credit window |
flagged | The claim came after the window closed. We record it with the flag outside_window so you can decide |
ignored | This installer already holds a credit. The answer carries duplicate_of, the referral that holds it |
none | No referral link brought this install. Nothing is recorded |
A tap on a device that already has the app is not a referral, unless Count people who already have the app is on for the app. It is off by default. An install from an ordinary link, or a tap from before the link became a referral link, answers none. A claim that names a link_id of an ordinary link answers 409 LINK_NOT_REFERRAL and records nothing.
Claim from the SDK or from your backend
Both record the same referral. A claim from the SDK needs no server work: the SDK sends the evidence it holds and the answer comes back to the app. An SDK key can call this route, and the SDK routes are limited to 60 requests a minute per key per client IP address.
For rewards of value, claim from your backend. Your backend holds your rules and your records, so it is the right place to decide when a sign-up has earned a reward. Send your server the attributionId from the SDK's attribution result together with the new user's installer id, then call POST /v1/referrals/claims with an API key that has referrals:write, passing the id as install_id. You can also send a recovery_token or a link_id in its place.
Exact and estimated matches
Every referral records how we matched it.
| Match | match.method | exact | Comes from |
|---|---|---|---|
| Invitation on iPhone | install_token | true | The invitation the person pasted |
| Install referrer on Android | android_referrer | true | The invitation inside the Play referrer |
| Link open | link_open | true | A tap on a device that already had the app, when existing users count |
| Estimated match | probabilistic | false | Device signals, with a confidence |
An estimated match is iPhone only. It applies while the iOS handoff is off and probabilistic matching is on for the app. It goes out as a referral.claimed event with exact set to false and a confidence.
If the same installer later claims with an exact match, for example after they update to 1.2.0 and paste the invitation, the referral keeps its id and moves to the exact link. We send referral.updated with previous showing what it replaced. An exact claim is never replaced by an estimate. Use exact and evidence_class (exact, estimated or legacy_link) to decide how a referral counts in your rules.
Receive the webhook
Add an endpoint under Referral handoff, then Webhook, and verify each event with its signature. Receive referral webhooks has the payloads, signature code for Node.js and Python, retries, and resends.
Test on your own phone
- Create a referral link and turn on the app's handoff.
- Delete the app from the phone, so the tap counts as a first install.
- Open the link on the phone. An iPhone shows the invitation page, then the App Store or a TestFlight build.
- Install, open the app, and tap the paste button.
- Sign up as a new test user with a new installer id, and claim.
- Open Referral handoff, then Referrals, for the new row, and confirm your endpoint received the event.
The first valid claim per app and installer id wins, so use a fresh installer id for each run. Delete a test referral with DELETE /v1/referrals/{id} or the MCP tool delete_referral to start over. Send test on the webhook tab checks your endpoint without a phone.
The Referrals report
The report lives at /referrals in the dashboard and in each app's Referral handoff tab. It lists referrals newest first, with the link, the installer id, the match, and the webhook result. The same list is GET /v1/referrals and the MCP tool list_referrals, and format=csv exports up to 10,000 rows.
| Status | Meaning |
|---|---|
claimed | Recorded. No webhook delivery has finished yet |
delivered | Your endpoint answered 2xx |
retrying | A delivery failed and another attempt is scheduled |
failed | Every attempt failed. Resend it after you fix the endpoint |
flagged | Claimed after the credit window |
duplicate | A later claim for an installer that already held a credit. It points at the first |
Open a referral to see its claims, the webhook events with the payloads we sent, and every delivery attempt. When an exact claim replaced an estimate, both claims and both events are listed.
Troubleshooting
A claim answers none and you expected a credit. Check the link first: it must be a referral link, and the tap must be after referral_since. Then check the window, and whether the person already had the app, which counts only when Count people who already have the app is on.
409 LINK_NOT_REFERRAL. The link_id names an ordinary link. Turn on is_referral for it, or leave out the link_id and let the evidence decide.
403 SDK_KEY_ROUTE_FORBIDDEN. An SDK key reached a route other than the four SDK routes. Use an API key with referrals:read or referrals:write for the report and webhook routes.
RECOVERY_TOKEN_EXPIRED or RECOVERY_TOKEN_INVALID. RECOVERY_TOKEN_EXPIRED means the invitation ran out or its link is paused or ended. RECOVERY_TOKEN_INVALID means the text was changed, is not complete, or belongs to another app. Ask the friend to open the link again, or ask the inviter for a new one.
The iOS switch will not turn on. Setup lists what is missing. Add the App Store URL, or ship a build on SDK 1.2.0 and open it once so we see it.
Next
- Receive referral webhooks
- Referrals API and Webhooks API
- SDK pages: iOS, Android, React Native, Flutter
Find your app identifiers
Where to find your iOS bundle ID, Apple Team ID, App Store URL, Android package name, and SHA256 fingerprint, the values Universal Links and App Links need.
Receive referral webhooks
Receive a signed webhook when a referral is claimed, flagged or upgraded. Verify the signature in Node.js or Python, handle retries, and resend events.