Pick your framework and auth provider. Get the exact server-side capture code, with the correct idempotency key so retries never double-count, plus a prompt you can hand to your coding agent.
// simplereferral referral attribution for Next (App Router) + Clerk (fetch mode).
// simplereferral API contract (private beta):
// POST ${SIMPLEREFERRAL_BASE_URL}/v1/signups
// Authorization: Bearer <SIMPLEREFERRAL_API_KEY> (server-side only)
// body { externalUserId, code } Idempotency-Key: signup-<externalUserId>
// Ignore code_not_found | self_referral | already_attributed (never block signup)
// Access: private beta. Get an early-access API key at https://simplereferralapp.com
// 1. Shared capture helper (server-side only)
// Shared capture helper. Server-side only, never ship the API key to a client.
// The Idempotency-Key derives from the account id (signup-<id>): it is stable
// per account, so a double-fired signup handler replays instead of double
// counting. Never key on a timestamp or a random value.
async function captureReferral(externalUserId, code) {
if (!code) return // no ref present, nothing to attribute
try {
const res = await fetch(`${process.env.SIMPLEREFERRAL_BASE_URL}/v1/signups`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SIMPLEREFERRAL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `signup-${externalUserId}`,
},
body: JSON.stringify({ externalUserId, code }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
// Swallow expected referral edge cases so a bad or absent ref never
// blocks signup: code_not_found, self_referral, already_attributed.
const swallow = ['code_not_found', 'self_referral', 'already_attributed']
if (swallow.includes(err.error?.code)) return
}
} catch {
// network error: never block signup
}
}
// 2. Capture the ?ref= code across the auth round trip (Next (App Router))
// middleware.ts: capture ?ref= into a cookie that survives the auth round trip.
import { NextResponse } from 'next/server'
export function middleware(request) {
const ref = request.nextUrl.searchParams.get('ref')
const res = NextResponse.next()
if (ref) res.cookies.set('sr_ref', ref, { path: '/', maxAge: 60 * 60 * 24 * 30 })
return res
}
// 3. Fire capture when Clerk creates the account
// Mount as a Route Handler at app/api/webhooks/clerk/route.ts, verifying the Svix signature first.
// Clerk fires attribution on the user.created webhook (the server-side
// signal for a new account). Forward the ref code through unsafeMetadata at signup:
// signUp.create({ ...fields, unsafeMetadata: { sr_ref: srRefFromCookie } })
async function onClerkUserCreated(request) {
// verifyClerkWebhook: implement with svix or @clerk/backend verifyWebhook
const evt = await verifyClerkWebhook(request) // Svix-verified Clerk event
if (evt.type !== 'user.created') return
const externalUserId = evt.data.id
const code = evt.data.unsafe_metadata?.sr_ref
await captureReferral(externalUserId, code)
}
Clerk fires attribution on the server-side user.created webhook, not a client callback. Forward the ref code through unsafeMetadata at signup and read it back in the webhook.
simplereferral owns the referral loop up to money: mint links, capture signup attribution, and send invites from shared infrastructure. Get an early-access key.