Skip to main content
Documentation
Guides

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.

A referral webhook tells your backend the moment a friend's install is credited to an invitation link. You grant the reward on your side, in your own system, from a signed event that arrives within seconds of the claim. This page covers the events, how to verify them, and what to do when something does not arrive. To create the invitation links and claim installs, read Referrals.

Add an endpoint

Open the app in the dashboard, go to Referral handoff, choose the Webhook tab, and add an endpoint. You can also use the Webhooks API or the MCP tools create_webhook_endpoint, rotate_webhook_secret, send_test_webhook and list_webhook_deliveries.

  1. Enter a public https address. Localhost, private networks, addresses with a user name or password, and WarpLink addresses are refused.
  2. Copy the signing secret. It starts with whsec_ and is shown in full once, when you create the endpoint or rotate it. Later reads show a masked form.
  3. Keep referral.flagged events on, which is the default, or turn them off for this endpoint.
  4. Send a test event.

Free plans have 1 endpoint per app and paid plans have 5. Only an owner or admin can manage endpoints, and an API key that manages them needs the referrals:write scope and a creator who is an owner or admin.

Events

EventSent when
referral.claimedAn install is credited to a referral link inside the credit window
referral.flaggedAn install is credited after the credit window closed. Sent to every endpoint unless you turn it off
referral.updatedAn exact match replaced an earlier estimate for the same installer
webhook.testYou pressed Send test or called the test route

Every event is a JSON object with the same four fields:

FieldMeaning
idThe event id. It is the same on every retry and every resend
data.referral_idThe referral. It stays the same when an exact match upgrades an estimate
data.linkThe invitation link that earned the credit, with its id, slug and full url
data.installer_idThe id you sent when you claimed. We store it as written
data.matchHow we matched. method is install_token, android_referrer, device_id, link_open or probabilistic. exact is false for an estimate, and confidence is a number only for an estimate
data.claimed_viasdk or api
data.flagsEmpty, or ["outside_window"] for a referral.flagged event

The installer_id is the id you chose for the new user, never a device identifier. See Referrals for what to send.

Updated referrals

An estimate is the best match we can make from device signals when an iPhone has no invitation to read. When the same installer later claims with an exact match, the referral keeps its referral_id, moves to the exact link, and we send referral.updated. It carries the new data plus the reason and what it replaced:

The example shows the fields that differ. The real event also repeats every field of data from the first example, with the new link and match. Treat previous as the old state to undo: take the credit away from the previous link's owner and give it to the owner of the new link. An exact claim is never replaced by a later estimate.

One credit per installer

The first valid claim for an installer and app earns the credit. A later claim for the same installer, through the same link or another, is a duplicate. We record it in the report with a link to the first referral, and we send no webhook for it. The one exception is the upgrade above: an exact claim replaces an earlier estimate and sends referral.updated. A second estimate stays a duplicate. See Referrals for the claim answers.

Verify the signature

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 signature is an HMAC-SHA256 of the timestamp, a period, and the raw request body: ${timestamp}.${rawBody}. The key is the whole signing secret, including the whsec_ prefix. Follow these steps in order:

  1. Read the raw body bytes before any JSON parser touches them. A re-serialized body does not match.
  2. Reject the request when the timestamp is more than 300 seconds from your clock.
  3. Compute the HMAC and compare it to each signature in the header with a constant-time comparison. The header holds a second signature during the 24 hours after a rotation, so accept a match on any of them.
  4. Answer with a 2xx status.

Do not parse the body before you verify it. Frameworks that decode JSON for you, such as express.json(), hide the raw bytes the signature covers. Use the raw body reader on this route.

Handle events once

We deliver each event at least once. A retry, a resend, or a slow answer from your server can put the same event in front of you twice, and the id is how you tell. Store the event id with the reward you granted, and skip an id you have seen.

Look up the person by installer_id, then find the owner of the invitation from link.id. Both come from your own records, so no personal data travels in the event.

Answer quickly. We wait 10 seconds for a reply, and a slower answer counts as a timeout. Do the slow work after you answer, from a queue.

Retries

A 2xx answer marks the event delivered. We send an event up to 5 times:

AttemptSent
1At once
230 seconds after the first
32 minutes after the first
48 minutes after the first
520 minutes after the first

A 5xx answer, a timeout, or a failed connection triggers the next attempt. Any other answer, a 4xx included, ends the event as failed, and so does the fifth attempt. We do not follow redirects, so a 3xx answer fails the attempt. Point the endpoint at its final address.

The event keeps the same id through every attempt. The dashboard shows each attempt with its status code, duration and error.

Resend an event

A failed or finished event can be sent again, with the same event id and the same payload, for 7 days after its last attempt. Open the referral in the Referrals report and choose Resend, or call the API:

POST /v1/referrals/{id}/resend sends the newest event of every endpoint for one referral, and takes an optional endpoint_id. An event still in flight answers 409 CONFLICT, and an event older than 7 days answers 410 GONE. Resending needs the referrals:write scope and a key whose creator is an owner or admin.

Test your endpoint

Send test sends one signed webhook.test event to the saved address and shows what your server answered. The event has the same shape and the same headers as a real one, with an id that starts evt_test_. We send it once and never retry it. It appears in the delivery log.

Rotate the secret

Rotate from the endpoint's menu in the dashboard, or with POST /v1/webhook-endpoints/{id}/rotate. The response carries the new secret once. The old secret keeps signing beside the new one for 24 hours, and the signature header lists both, so deploy the new secret to your receiver at your own pace. A second rotation inside that window ends the first old secret at once.

Limits

LimitValue
Endpoints per app1 on Free, 5 on paid plans
Attempts per event5, over 20 minutes
Reply timeout10 seconds
Timestamp tolerance300 seconds
Resend window7 days after the last attempt
Secret overlap after rotation24 hours
AddressPublic https only

If a plan change allows fewer endpoints, the extra ones pause and stop receiving events until you choose which to keep.

Next