Set up referrals
Set up referrals — a runnable, end-to-end guide verified against the VINR sandbox.
This guide builds a working referral program on top of VINR Engagement: each existing customer gets a unique code, new customers attribute on signup, and both sides earn loyalty points when the referred customer makes their first qualifying payment. It's runnable against the sandbox — swap your test keys in and follow along.
OverviewAsk
A referral is a reward that fires on a delayed condition: you grant the points only after the invited customer converts. That two-step shape is what makes referrals different from a normal earn rule.
referrer VINR referred customer
│ share code │ │
│─────────────────────────────────────────►│ signs up with code
│ │ attribute referral │
│ │◄──────────────────────│
│ │ │ first payment
│ │◄──────────────────────│
│ loyalty.points.earned (both accounts) │
│◄─────────────────│──────────────────────►│You create a referral program once, mint a code per referrer, attribute it at signup, and let a webhook settle the reward on the referred customer's first payment.completed.
Create the referral programAsk
A referral program is a loyalty program (prog_) configured with a referral ruleset. It defines who earns, how much, and what counts as a qualifying conversion.
import { Vinr } from '@vinr/sdk';
const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });
const program = await vinr.loyalty.programs.create({
name: 'Refer a friend',
type: 'referral',
currency: 'EUR',
referral: {
referrerReward: { points: 500 }, // paid to the inviter
refereeReward: { points: 500 }, // paid to the new customer
qualifyingEvent: 'payment.completed',
minimumAmount: 2000, // €20.00 first purchase
rewardOn: 'first_qualifying_payment',
expiresAfterDays: 30, // code attribution window
},
});Prop
Type
Generate a referral code per customerAsk
Mint one durable code per referrer's loyalty account (loy_). Codes are idempotent on the account, so calling this again returns the same code rather than creating duplicates.
const code = await vinr.loyalty.referrals.createCode(
{
programId: program.id, // prog_...
accountId: referrerAccountId, // loy_...
},
{ idempotencyKey: `refcode-${referrerAccountId}` },
);
// Build a shareable link your frontend can render
const shareUrl = `https://yoursite.com/signup?ref=${code.code}`;Don't generate a fresh code per page load. One stable code per account keeps analytics clean and lets the referrer reuse the same link everywhere. See Loyalty accounts.
Attribute the referral at signupAsk
When a new customer arrives with ?ref=, attribute the code to their new customer (cust_) the moment you create their loyalty account. Attribution only pends the reward — nothing is paid until they convert.
export async function POST(req: Request) {
const { email, refCode } = await req.json();
const customer = await vinr.customers.create({ email });
const account = await vinr.loyalty.accounts.create({
programId: process.env.VINR_LOYALTY_PROGRAM_ID,
customerId: customer.id,
});
if (refCode) {
await vinr.loyalty.referrals.attribute({
programId: program.id,
code: refCode,
refereeAccountId: account.id,
});
}
return Response.json({ customerId: customer.id });
}Attribution fails if the code has expired (past expiresAfterDays) or if the referee is the same account as the referrer (self-referral). Both surface as a 4xx — catch and degrade gracefully rather than blocking signup.
Settle the reward on conversionAsk
The referred customer's first qualifying payment is what releases points to both sides. Listen for loyalty.points.earned so you can notify each party — but VINR does the granting for you once the referral.qualifyingEvent fires.
export async function POST(req: Request) {
const event = vinr.webhooks.verify(
await req.text(),
req.headers.get('x-vinr-signature'),
);
switch (event.type) {
case 'loyalty.referral.converted':
// both ptx_ point transactions already created by VINR
await emailReferrer(event.data.referrerAccountId);
await emailReferee(event.data.refereeAccountId);
break;
case 'loyalty.referral.rejected':
// failed fraud checks — log for review
await logRejection(event.data.referralId, event.data.reason);
break;
}
return new Response('OK', { status: 200 });
}Fraud controlsAsk
Referral programs attract abuse. VINR enforces a few controls automatically, and you should layer your own on top:
- Self-referral blocking — attribution rejects when the referrer and referee resolve to the same customer.
- Conversion gating — the
minimumAmountandfirst_qualifying_paymentsettings stop reward farming via tiny or repeat purchases. - Velocity limits — set
maxConversionsPerCodeon the program to cap how many times one code can pay out. - Manual review —
loyalty.referral.rejectedevents expose areason(e.g.duplicate_device,disposable_email) so you can investigate from Operations.
await vinr.loyalty.programs.update(program.id, {
referral: { maxConversionsPerCode: 25, requireDistinctPaymentMethod: true },
});Test itAsk
In the sandbox, run the full loop: create a referrer account, mint a code, attribute it to a new account, then pay with a qualifying card.
| Sandbox action | Card | Expected |
|---|---|---|
| First payment ≥ €20.00 | 4242 4242 4242 4242 | Both accounts earn 500 points |
First payment below minimumAmount | 4242 4242 4242 4242 | No reward, no event |
| Declined first payment | 4000 0000 0000 0002 | Referral stays pending |
Next stepsAsk
Earn loyalty at checkout
Award points on a payment the customer just made.
Loyalty programs
Tiers, earn rules, and reward catalogs.
Engagement overview
The full loyalty and engagement model.
Last updated on