Conversion tracking
Detecting an incoming referral is automatic. Telling Roastnest that the referral turned into something — a signup, a subscription, a purchase — is one hook call.
The flow
A referral has two halves, and only the second one needs code from you.
- Detection, automatic. A visitor arrives on a link carrying
?ref=CODE. The SDK reads it, stores it in both a cookie andlocalStorage, and keeps it for 30 days by default. It also recognises?referral=and?invite=. - Conversion, yours to call. When that visitor does the thing you care about, call
trackConversion. The stored code, visitor ID, session ID and device metadata are attached for you.
Detection survives navigation
Because the code is persisted rather than read from the current URL, a visitor can land on your pricing page, browse for a week and sign up from a bookmark — the attribution still holds.
Tracking a conversion
Call the matching track.* method at the point of success — after the account exists, not when the form is submitted.
import { useEffect } from "react";
import { useReferral } from "@roastnest/react";
export function SignupSuccess() {
const { track, hasReferral, referralCode } = useReferral();
useEffect(() => {
if (hasReferral) {
track.signup();
}
}, [hasReferral]);
return <h1>Welcome aboard{referralCode ? " — referral applied" : ""}!</h1>;
}Conversions can carry a value, a currency and arbitrary metadata:
const { track } = useReferral();
await track.purchase({
value: 49.0,
currency: "USD",
metadata: { plan: "pro", billing: "annual" },
});Identifying the referee
A conversion with no identity is recorded for analytics but can never trigger a reward — there is nobody to grant one to. To name the person who converted, put id (your own user id) or email in the metadata. Either is enough on its own, exactly as with the referrer.
const { track } = useReferral();
await track.purchase({
value: 49.0,
currency: "USD",
metadata: {
id: user.id, // your own user id - enough on its own
name: user.name,
email: user.email, // optional if you sent an id
},
});Whichever you send comes back on the referral.converted and referral.reward_granted webhooks, so a project that never shares emails can still match a payout to a user.
Self-referrals are dropped
A referrer converting their own link is rejected with a 400 rather than recorded. It is meaningless attribution and an easy way to farm rewards.
Custom events
For event names not covered by the built-in methods, use track.custom(). It will reject names that overlap with the built-in catalog to prevent accidental shadowing.
// For event names not in the built-in catalog
const { track } = useReferral();
await track.custom("app_installed", {
metadata: { platform: "ios" },
});Guard against double counting
Each track.* call sends an event every time it is called. Fire it from a page the user reaches once, or gate it behind a flag you persist, so a refresh does not credit the referrer twice.
Migrating from trackConversion
The old trackConversion({ event: "signup" }) API still works but is deprecated. Replace it with the corresponding track.* method for type safety and autocompletion.
useReferral
The hook is headless — it reads from the referral API instance the widget initialises, so a ReferralWidget must be mounted somewhere in the tree for it to have state to report.
| Prop | Type | Description |
|---|---|---|
referralCode | string | null | The detected code, or null if this visitor arrived directly. |
hasReferral | boolean | Convenience boolean for the same thing. |
referralData | ReferralData | null | The code plus where it came from (query, cookie or localStorage) and when it was first seen. |
track | ReferralTracker | Type-safe namespace with track.signup(), track.purchase(), track.subscription(), track.trial_started(), and track.custom() methods. |
trackConversion (deprecated) | (event: ConversionEvent) => Promise<void> | Deprecated. Use track.* methods instead. Kept for backward compatibility. |
isTracking | boolean | True while a conversion request is in flight. |
clearReferral | () => void | Forgets the stored referral — use after a conversion that should not be re-attributed. |
queuedEvents | QueuedEvent[] | Events that failed to send and are awaiting retry. |
retryQueue | () => Promise<void> | Flushes the retry queue immediately. |
visitorId | string | Stable per-browser identifier. |
sessionId | string | Identifier for the current session. |
referrerIdentity | ReferrerIdentity | undefined | The identity currently attached to outgoing referral links. |
setReferrerIdentity | (identity) => void | Attaches or replaces the referrer identity after login. See Referral widget. |
The event payload
Every referral event — copies, shares and conversions alike — is delivered in the same shape. In cloud mode it goes to Roastnest; in self-hosted mode it goes to your onEvent callback.
{
projectId: "prj_123",
referralCode: "AYUSH123",
event: "purchase",
value: 49,
currency: "USD",
metadata: { plan: "pro" },
visitorId: "vis_9f2c...",
sessionId: "ses_41ab...",
currentPage: "https://myapp.com/checkout/success",
referrerUrl: "https://myapp.com/pricing",
browser: "Chrome",
os: "macOS",
device: "desktop",
timestamp: "2026-01-14T09:22:31.004Z"
}Self-hosted handling
<ReferralWidget
mode="self-hosted"
referralLink="https://myapp.com/invite"
onEvent={async (payload) => {
await fetch("/api/referral-events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}}
/>The retry queue
A conversion that fails to send is not lost. Failed events are written to localStorage and retried with backoff — three attempts, spaced roughly one, three and ten seconds apart. Anything still unsent survives a reload and is retried on the next mount.
You can inspect and flush the queue yourself, which is occasionally useful right after the browser comes back online:
const { queuedEvents, retryQueue } = useReferral();
if (queuedEvents.length > 0) {
await retryQueue();
}Configuration
Two details of detection are adjustable through the referral config — queryParam, which defaults to "ref", and cookieDurationDays, which defaults to 30. Change the parameter name if ref already means something else in your URLs; the fallback names are still recognised either way.
For the widget props that surround this — reward copy, sharing, identity — see Referral widget.