Signup tracking with any auth system
Connect signups to referral clicks from any authentication system, with one or two backend calls and no storage in your application.
RefRef connects a signup to the referral click that came before it. Your authentication system can be your own code, a library, or a hosted login page. The pattern is the same for all of them: at the moment the account is created, your backend tells RefRef which browser created it. RefRef keeps every referral record. Your application stores nothing for referrals.
Set up the landing script first. It puts the browser's referral handle in the refref_handle cookie.
The two calls
Your backend makes these calls with a Workspace API key in the X-Api-Key header. Never call them from the browser.
| Call | When |
|---|---|
POST /v1/track/events with type: "signup" | When the person counts as signed up |
identify: { handle } on that signup, or POST /v1/identify | Once, when the account is created, from the browser that created it |
Every recipe below is a combination of these two calls. The helper below is used in the examples:
const REFREF_API = "https://api.refref.ai";
async function refref(path: string, body: object) {
const response = await fetch(`${REFREF_API}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.REFREF_API_KEY!,
},
body: JSON.stringify({
projectId: process.env.REFREF_PROJECT_ID,
...body,
}),
});
if (!response.ok) throw new Error(`RefRef ${path}: ${response.status}`);
return response.json();
}A signup also needs sourceNamespaceId, the ID that POST /v1/sources returns when you register your application as a source. Name each person with your own stable user ID: { kind: "individual", externalId: user.id }. RefRef never matches people by email.
In every recipe, send identify only when the request has a refref_handle cookie. A browser without the cookie did not follow a referral link, and an empty identify fails validation. The examples read the cookie once:
const handle = cookies.get("refref_handle");Recipe 1: your backend creates the account
Use this when the account is created in a request from the person's browser, for example an email and password form, or a social login that returns to your own domain. The request carries the refref_handle cookie, so one call does everything.
// In the request that created the user
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: user.id },
...(handle && { identify: { handle } }),
});A cookie value that is not a handle never fails the signup: the response then has identified: false. A person has one signup, so you do not need an event ID or a time.
Recipe 2: the person must verify their email first
Use this when a signup should count only after email verification. Identify the browser when the account is created, and send the signup later. An account that is never verified has no signup and no referral.
// When the account is created
if (handle)
await refref("/v1/identify", {
handle,
participant: { kind: "individual", externalId: user.id },
});
// When the email is verified, from any device
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: user.id },
});Identify only the browser that created the account. If someone can create an account with another person's email, identify it only after the creator proves the email, or use a reference as in recipe 3.
Recipe 3: magic links and email codes
A magic link or an email code can be opened on another device, where the cookie is missing. Use a reference: a key of that one login challenge. When the challenge is sent, identify the browser to the key. When the account exists, resolve the key to the person.
Derive the key from the challenge itself, so you do not store it. For example, use an HMAC of the magic-link token with a server secret. Do not use the raw token, the email address, or a short code alone as the key.
import { createHmac } from "node:crypto";
const referenceKey = (challenge: string) =>
createHmac("sha256", process.env.APP_SECRET!).update(challenge).digest("hex");
// When you send the link, in the browser's request
if (handle)
await refref("/v1/identify", {
handle,
reference: { key: referenceKey(magicLinkToken) },
});
// When the link creates the account, on any device
await refref("/v1/identify", {
reference: { key: referenceKey(magicLinkToken) },
participant: { kind: "individual", externalId: user.id },
});
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: user.id },
});A reference resolves once and expires after 24 hours by default (expiresAt can be up to 90 days). A key is never used again. When the link is used on an existing account, do not resolve: a login is not a signup.
Recipe 4: a hosted login page
Use this when the login page is on the provider's domain and returns to a callback on your domain. Most callbacks are top-level redirects, so the browser sends the refref_handle cookie. Identify in the callback, but only when the provider says the account is new.
// In your login callback route
if (isNewAccount) {
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: user.id },
...(handle && { identify: { handle } }),
});
}Use a signal from the provider for isNewAccount, such as a first-login count or a "user created" flag. An account creation time is only a hint.
If the callback can arrive on a host without the cookie, use a reference as in recipe 3. Before the redirect, identify the handle to a key derived from the login's OAuth state, which your callback checks against the starting browser. In the callback, resolve the same key to the new account.
Recipe 5: accounts created by a provider webhook
Use this when your application learns about new accounts from a webhook. A webhook has no browser cookie. Give the handle to the provider when the signup starts, for example in the signup's metadata. Read it back from the verified webhook.
// In your verified "user created" webhook handler
const handle = event.data.metadata?.refrefHandle;
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: event.data.id },
...(typeof handle === "string" && { identify: { handle } }),
});Verify the webhook signature first. Return an error status when RefRef fails, so the provider sends the webhook again. A repeated signup for the same person returns the first result.
Recipe 6: company accounts
Use this when the referred customer is a company, team, or workspace. Name the company as a group participant and identify the creator's browser when the company is created. The person who created it is the actor. Members who join later never identify.
// In the request that created the company
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "group", externalId: company.id },
actor: { kind: "individual", externalId: user.id },
...(handle && { identify: { handle } }),
});Choose one: people or companies. If a company is created automatically at signup, identify only the participant that your Programs reward.
Recipe 7: referral codes typed by the person
When the person types a referral code at signup or checkout, send it on the Event in refCode. An Event carries at most one code. A valid code wins over every click.
await refref("/v1/track/events", {
sourceNamespaceId: process.env.REFREF_SOURCE_NAMESPACE_ID,
type: "signup",
participant: { kind: "individual", externalId: user.id },
refCode: form.referralCode,
});What never identifies
- A login, a session refresh, an email verification, or linking another login method to an existing account.
- An account that an administrator creates or that you import. Send no
identifyfor these accounts. - A purchase. Purchases carry no browser data; they use the person's existing referral.
When a call fails
- Retry the same request body. A repeated signup or identification returns the first result.
- A failed RefRef call must not fail your signup. The example helper throws on an error status, so call it in a
try/catch, log the error, and retry later with the same body. POST /v1/identifyanswers404 NOT_FOUNDwhen the cookie value is not a handle of the Project, and409 CONFLICTwhen the handle is already identified, or the reference has expired or already resolved. Do not retry these.- Send the identification before the signup or with it. A signup does not wait for a later identification.
Check your integration
| Test | Expected result |
|---|---|
| Person A shares a link; person B follows it and signs up | B's signup has a referral from A |
| B signs in again later | No second signup and no change to the referral |
| An existing user follows a referral link and signs in | No referral |
| B requests a magic link on a laptop and opens it on a phone | The referral from the laptop's click (recipe 3) |
| B never verifies their email (recipe 2) | No signup and no referral |
| You inspect the browser bundle and requests | No API key |
If you use Better Auth, the RefRef Better Auth plugin will make these calls from its configuration. Its package is not published yet.