Skip to main content

Referral programs: issuance, attribution, leaderboard, and edge cases

This is the deeper reference behind the end-to-end Referral programs guide. That guide walks the workflow; this page explains what the surface actually is, and then covers each mechanic — code issuance, conversion attribution, the live leaderboard, reward fulfilment, and the edge cases — one section at a time.

1. What the referrals surface is

Referrals is a tenant-defined program you run for your own end-users, not Orbit’s own referral scheme and not the same surface as the incentives engine. The split matters: incentives covers one-off promos — issue, redeem, or void a standalone reward, a one-time payout from the console. A referral program is a repeatable funnel — a public redirect that mints a per-contact code and records a conversion each time a new signup arrives through one. The referral program fulfills its reward through the incentives engine when you approve a conversion, but defining the program itself is a separate concern from issuing a single promo. Each program has its own reward type, its own set of issued codes, and its own conversions ledger. You define the program once (name, reward, optional bounding window, landing URL), then you issue one unique code per referrer contact, then conversions accrue, then you approve the payouts.

2. Issuing codes and assigning them per contact

One unique code per referrer is the assignment model, and the endpoint enforces it. POST /api/v1/referrals/programs/{id}/codes is mint-or-get: call it with a contact_id and it returns the existing code for that contact on the program rather than minting a duplicate. That is what makes per-contact assignment safe to trigger from an automation — re-running the issue step for a whole cohort never fragments a referrers’ link. Build the shareable link from the returned code: https://api.orbit.devotel.io/r/AbCdEf23.
Because issue is mint-or-get per contact, the same referrer always lands on their one stable code across every program they belong to — the leaderboard row, the public redirect, and the contact’s record all point at the same code.

3. What counts as a conversion, and how redemption is attributed

A conversion is a contact created while carrying a referral code. Your signup flow calls the normal POST /api/v1/contacts; the handler reads the ref_code (or ref) query parameter first, then falls back to the ref_code cookie, and the referral conversion logic runs as a side effect. No separate “record a referral” endpoint exists. The public redirect GET /r/:code stamps attribution in two carriers so the code survives both in-app and off-site flows:
  1. Query parameter — the redirect appends ?ref_code=<code> to the program’s target_url. This is the primary carrier because it survives a jump to your own external signup page, where a cookie set on Orbit’s API host never round-trips back.
  2. Cookie — an HttpOnly ref_code cookie (30-day TTL) carries the code when the prospect stays on a surface that round-trips to the API host.
Inside the platform, each click resolves the code to a tenant and program, bumps the code’s visits counter, and — when the new contact is created with the referral context present — records a row in the program’s conversions ledger with a reward_status of pending, paid, or rejected.

4. The live leaderboard operators monitor

GET /api/v1/referrals/programs/{id}/leaderboard?limit=25 ranks the program’s codes by conversions. Each row is a referrer code with its visits, signups, and rewards_paid counters plus the referrer’s human-readable label alongside the raw contact id. The screen mirrors this: the program detail view shows stat cards pulled from /totals (authoritative over the whole program — the leaderboard page is capped at 100 rows), the leaderboard table ranked by conversions, and the conversions table with each signup’s reward status. limit defaults to 25 and maxes at 100, so the leaderboard page is a cap, not a guarantee — for a program-wide signup total use /totals, not a sum over the leaderboard page.

5. Reward fulfilment — the credit or promo paying out of the referral

The reward fulfils through the incentives engine when an operator approves a pending conversion. The three payout paths:
  • credit — Orbit adds the configured per-side amount as wallet credits on your tenant’s billing account, one per side.
  • discount_code — the incentives engine mints a unique redeemable code and returns it on the pay response.
  • custom — a structured payout record (status manual, reason awaiting_tenant_fulfillment) is issued through the engine so a tracked record exists for whatever process fulfills it.
Every payout is audit-logged with the referrer, referee, reward type, and fulfillment summary; the minted discount-code string itself is kept out of the audit trail. To enumerate every reward your program has paid out, filter the issued-incentives ledger by source=referral via GET /incentives/issued.

6. Edge cases: duplicate redemptions, self-referral blocks, reward revocation

These are the paths a public-facing offer with a real reward attached has to handle — and the conversions ledger records the reject reason instead of silently dropping the row. Duplicate redemptions. The conversion insert is idempotent per (program_id, referee_contact_id): re-running the same signup with the same code returns duplicate_referee and does not double-count. A re-click on an issued code also returns the same single code, because issuance is mint-or-get. Self-referral blocks. Two fraud guards reject with a structured reason: same_ip_as_referrer (the visit and signup came from the same IP), and self_refer_rapid_signup (the referrer and referee contacts were created within the rapid self-refer window). Rejected rows are recorded with reward_status: "rejected" and their reject_reason — a suspicious burst shows up in the conversions list rather than disappearing; they are not paid out. Reward revocation. There is no “unpay” path on a paid conversion — the status enum is pendingpaid or rejected, and the ledger row records what happened. The revocation story lives on the fulfilment side: an issued incentive (including the reward a referral payout minted) can be voided through the incentives ledger that powers payout fulfilment. See the incentives ledger guide for the issued / redeemed / voided lifecycle that referral rewards ride on. Bounded windows and disabled programs. Outside start_at / end_at, referrals are still visited but come back with program_not_started / program_ended reject reasons; a disabled program returns program_disabled. Prefer disabling over deleting — deletion cascade-removes the program’s codes and conversions.