OfferKitDocs

Launch a referral program

Launch OfferKit referrals with stable advocate codes, link rewards for both customers, record real conversions, and recover incomplete reward delivery.

In a referral program, an existing customer shares a code, a friend uses it, and once the friend does something that counts (usually paying for a first order) both of them get a reward.

These docs call the person sharing the code the advocate and the person using it the friend. The API calls them the referrer and the referee, so you will see referrerCustomerId, refereeCustomerId, referrerReward, and refereeReward.

Your application owns the share page, decides what counts as a success, and delivers the rewards. OfferKit owns the codes, blocks self-referral and double conversion, records each conversion, and issues the rewards.

How the pieces fit

REFERRAL_PROGRAM campaign
└── Referral program (at most one that is not deleted)
    ├── advocate reward and friend reward settings
    └── Referral codes   one per advocate, e.g. ALICE-7kQp3MzR
        └── Conversions  one per friend who converts through that code
            └── Rewards  vouchers created in this same campaign,
                         loyalty points, or a custom payload
RelationshipCardinality
Campaign to referral programAt most one program that is not deleted
Advocate to referral codeOne code per advocate per program
Referral code to conversionsMany; each friend at most once per code
Conversion to rewardsOne for the advocate and one for the friend, every time

So if Alice shares her code with ten friends and all of them convert, there are ten conversions, and Alice receives ten advocate rewards.

What the campaign controls

Referral calls themselves ignore the campaign’s status and dates. You can issue codes and convert friends while the campaign is a draft, paused, or past its end date.

The vouchers a conversion creates are different. They belong to the referral campaign, so when someone tries to use one, OfferKit checks that campaign like any other: it must be active, inside its start and end dates, in the same currency as the order, and it must pass the campaign’s validation rule and per-customer limit if you set them.

In practice:

  • Activate the campaign before you start converting. A conversion on a draft campaign succeeds and hands out codes that fail at checkout with campaign_inactive.
  • Ending or pausing the campaign stops existing reward vouchers from working. It does not stop new conversions. To stop the program, stop calling convert.
  • Every issued discount or gift voucher stops working at the campaign end date. The voucher’s own date fields remain unset, so changing the parent window affects them all.

Choose each side’s reward

Each side gets one reward, configured separately. Monetary values are in the minor unit of the referral campaign’s currency. Loyalty-point amounts are points.

Discount voucher

{ "kind": "discount", "discount": { "type": "AMOUNT", "amount": 1500 } }

Each conversion creates a new discount voucher in the referral campaign:

  • the code looks like REF-a8KxTq2mWz; the campaign’s code settings are not used;
  • it can be redeemed once;
  • it is assigned to the recipient, so checkout must identify that customer (by customerId or customerExternalId), or the redemption fails with customer_required or customer_mismatch;
  • it has no dates of its own and works only while the campaign is active and inside its dates;
  • it uses the campaign’s currency.

Percentage discounts use basis points: { "type": "PERCENTAGE", "percent": 1000 } is 10%.

Gift-card credit

{ "kind": "gift_card", "creditCents": 2000 }

Each conversion creates a gift-card voucher with that balance, a code like GIFT-Hm4rZp8sQe, and a ledger entry for the initial credit. It is assigned to the recipient, can be spent over several orders until the balance runs out, and follows the same campaign status, date, and currency rules as the discount voucher.

Loyalty points

{
  "kind": "loyalty_points",
  "loyaltyProgramId": "6f1c2a8e-3b7d-4c55-9e0a-1d2b3c4d5e6f",
  "loyaltyPoints": 500
}

The recipient gets 500 points in that loyalty program. They must already be enrolled; if they are not, the conversion fails with missing_loyalty_member. Only the sides that receive loyalty points need to be enrolled. If the advocate gets points and the friend gets a discount, only the advocate needs a membership.

The points arrive as an ADJUSTMENT entry in the member’s ledger, with the note referral referrer or referral referee. Because it is an adjustment, not an earning:

  • the tier multiplier is not applied;
  • lifetime points do not increase, so it does not help the member reach a tier;
  • the member’s tier is not recalculated;
  • the points never expire.

OfferKit does not check loyaltyProgramId when you save the referral program. A wrong ID first shows up as missing_loyalty_member at conversion time, so test a conversion before launch.

Custom reward

{ "kind": "custom", "typeKey": "FREE_MONTH", "payload": { "months": 1 } }

OfferKit returns the payload in the conversion result and does nothing else. Your application grants the free month. typeKey is required, but OfferKit does not check it against your reward types or check the payload’s shape.

Configure the program

  1. Create a campaign with type REFERRAL_PROGRAM in the currency you want rewards in.
  2. Open Referrals and create a program for that campaign.
  3. Set the advocate reward and the friend reward.
  4. Optionally change the code length (the random part of each advocate code, 8 by default).
  5. Activate the campaign.

Keep the program ID in server configuration. OfferKit API keys must never reach the browser.

If you edit the rewards later, the change applies to conversions from then on. Past conversions keep the outcome they were issued.

Keep customer identity in sync

Both the advocate and the friend have to be OfferKit customers, and referral calls take the internal customer ID. Upsert from your own user ID first:

const { customer } = await offerkit.customers.upsert({
  externalId: user.id,
  email: user.email,
  name: user.name,
});

This is safe to repeat. Run it when the account is created or just before a referral call. If either side receives loyalty points, enroll them in that loyalty program at the same time.

Give the advocate a code

const issued = await offerkit.referrals.issue({
  programId,
  referrerCustomerId: customer.id,
  prefix: "ALICE",
});

if (!issued.ok) throw new Error(issued.message);

Calling issue again for the same advocate and program returns the same code, including when two calls race. Without a prefix, OfferKit builds one from the customer’s name. The random part mixes upper- and lowercase letters, and codes are matched exactly, so keep the case as issued and do not change it when you store or compare codes.

Build share links on your own domain, for example https://shop.example/r/ALICE-7kQp3MzR.

Hold the code until the friend succeeds

When a friend opens the link or types the code, store it with their signup or checkout session. Do not convert yet. If you convert when the code is entered, abandoned carts and failed payments hand out real rewards.

To reject unknown codes early, look the code up first:

const referral = await offerkit.referrals.getByCode({ params: { code } });

This returns the code’s advocate and program, or a NOT_FOUND error.

Convert after the success event

Call convert from a trusted server-side event, such as your payment webhook or an order fulfillment job:

const conversion = await offerkit.referrals.convert({
  code: order.referralCode,
  refereeCustomerId: buyer.id,
  conversionEventId: `order:${order.id}`,
});

if (!conversion.ok) {
  return handleReferralFailure(conversion.errorCode, conversion.message);
}

What convert checks:

SituationResult
Unknown codereferral_not_found
The friend is the advocateself_referral
This friend already converted through this codereferee_already_converted
The program was deletedprogram_not_found
A loyalty reward recipient is not enrolledmissing_loyalty_member
A reward setting is incompletevalidation_error

What it does not check: the campaign’s status and dates, and whether either customer record has since been deleted. Do those checks in your own code if they matter.

Retries

Always send a conversionEventId. If you call convert again with the same code and event ID, you get the original conversion back with idempotent: true, and no new rewards are issued. Without an event ID, a retry for the same friend returns referee_already_converted, and the original rewards are not in that response.

Concurrent calls with the same event ID can return a transport error while the other call completes. Check for the completed conversion before retrying with the original code and event ID. Do not replace the event ID to work around an error.

When one side fails

OfferKit issues the advocate’s reward first and the friend’s second. If the friend’s side fails, for example with missing_loyalty_member because the friend is not enrolled, the advocate’s reward may already have been created even though no conversion is recorded. Calling convert again after fixing the problem issues the advocate another reward.

To avoid this, make sure both sides can receive their reward before you convert: enroll any side that gets loyalty points, and run a test conversion for every reward configuration before launch. If you do see a friend-side failure, check the advocate’s vouchers in the referral campaign (offerkit.vouchers.list({ campaignId, customerId: advocateId })) or their loyalty history before retrying. Disable an extra voucher, or use a recorded loyalty adjustment when appropriate; there is no public referral-conversion rollback.

Deliver both rewards

A successful response includes referrerReward and refereeReward:

{
  "ok": true,
  "conversionId": "…",
  "referrerReward": { "kind": "discount", "voucherCode": "REF-a8KxTq2mWz" },
  "refereeReward": { "kind": "gift_card", "voucherCode": "GIFT-Hm4rZp8sQe" },
  "idempotent": false
}

Discount and gift-card rewards include the new voucherCode. Loyalty rewards include the loyaltyTransactionId of the points entry. Custom rewards include the payload.

Referral conversions do not send webhooks. Save the conversion result in your database first, then send emails or in-app messages from that saved record, keyed on conversionId. That way a retried job does not send the same message twice, and a crash between converting and notifying cannot lose the codes.

Retries and recovery covers the same patterns for the other commit calls.

The referral integration reference also shows how to qualify a customer’s existing vouchers when displaying available rewards.

Test before launch

  • Issue a code for the same advocate twice.
  • Convert two different friends through one code.
  • Try a self-referral.
  • Convert the same friend twice with different event IDs.
  • Replay the same conversionEventId.
  • Abandon or fail a payment and confirm no conversion happens.
  • Convert once for every reward kind you configured, then redeem each voucher at checkout as the right customer and as the wrong one.
  • Convert with a loyalty reward recipient who is not enrolled, and check what the advocate received.
  • Pause the campaign and confirm reward vouchers stop working.

Operate and migrate

The referral program page lists codes and conversions. Reconcile each conversion with its conversionEventId and both reward outcomes.

If you already have referral history, follow Migrate existing referrals. Do not call convert for past conversions, because that would issue the rewards again.

For the import queries, follow the SQL migration walkthrough.