Set up referrals

Set up referrals — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

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 minimumAmount and first_qualifying_payment settings stop reward farming via tiny or repeat purchases.
  • Velocity limits — set maxConversionsPerCode on the program to cap how many times one code can pay out.
  • Manual reviewloyalty.referral.rejected events expose a reason (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 actionCardExpected
First payment ≥ €20.004242 4242 4242 4242Both accounts earn 500 points
First payment below minimumAmount4242 4242 4242 4242No reward, no event
Declined first payment4000 0000 0000 0002Referral stays pending

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page