Referral program model
A referral program in Orbit is a named, tenant-owned reward definition that turns your contacts’ shared links into tracked signups. The model has four artifacts — program → code → conversion → reward — and two halves: a public, unauthenticated redirect (GET /r/:code) that anonymous prospects click, and an authenticated controller (/api/v1/referrals) that you use to define, watch, and pay out the program. This page explains that model; the Referral programs guide walks the operator workflow end to end.
1. What a program is
A program is the reward definition: one row inside your tenant that names the offer (name), what each side earns (reward_type, reward_value, and the optional per-side referrer_reward / referee_reward configs), where referred visitors land (target_url), and when the program is live (start_at / end_at, plus the enabled flag). Programs never cross tenant boundaries — every row lives in your workspace’s schema, and a projected-out prospect never sees another tenant’s program.
The reward type decides which rails a payout rides:
Omit
referee_reward for a single-sided program; set it and the referee is rewarded too when you approve the conversion. A program created with enabled: false accepts no conversions until you enable it; deleting a program cascade-removes its codes and conversions, so prefer disabling over deleting when the program might come back.
2. Codes: minted per contact, idempotent by design
A code is one referrer’s shareable identity — an 8-character, base58 string without ambiguous glyphs — minted withPOST /api/v1/referrals/programs/{id}/codes. The endpoint is mint-or-get: call it twice for the same contact and program and you get the same row back, so the “issue my link” button in your product can call it on every page load without fear of duplicates. Each code row carries its own visits, signups, and rewards_paid counters, so a weak channel and a strong one surface separately on the leaderboard.
The code’s public face is GET /r/:code — a root-mounted redirect that lives outside /api/v1 auth because the person clicking is a prospect, not an operator. The Public pixels, redirects, and short links concept documents the pixel-level contract of this surface (always-rendered responses, per-IP rate limits, branded error pages). The referral model’s claim on that redirect is what it does with the code: resolve it to a tenant and program, record a visit (IP, user-agent, geo), set an HttpOnly ref_code cookie with a 30-day TTL, then 302 to the program’s target_url with ?ref_code=<code> appended. The two /r/ route shapes coexist on one prefix — a single-segment code is a referral and a two-segment signed link is an email tracker — and the router disambiguates by path-segment count.
3. Lifecycle: program → issue code → conversion → reward
The artifacts chain through four states:- Program — defined once, enabled/disabled over its
start_at–end_atwindow. New conversions are rejected outside the window withprogram_not_started/program_endedreasons and disabled programs accept none. - Code issued — mint-or-get per contact; the code’s counters start at zero.
- Conversion recorded — when your signup flow creates a new contact with the
ref_codequery parameter or cookie present, the create-contact handler records a conversion under the program withreward_status: "pending". Fraud guards reject same-IP visit+signup and rapid-fire referrer/referee creation with a structuredreject_reason(recorded, not silently dropped). - Reward paid — an operator approves the pending conversion through
POST /programs/{id}/conversions/{conversionId}/pay, which flips the status topaidand fulfills the reward.rejectedrows never pay out.
pending, paid, rejected), and gated writes (create/update/delete program, issue code, pay reward) require the owner, admin, or developer role — the same guard the dashboard’s Marketing → Referrals screen enforces as owner/admin/billing. Reads (list, leaderboard, totals, conversions) need only authentication.
Leaderboard vs totals. The leaderboard answers “who is driving signups” as a ranked top-N page of codes (limit default 25, max 100 — a cap, not a complete enumeration). The totals endpoint answers “how big is the program” by aggregating every code with no page-size limit (distinct referrers, total signups, total paid rewards). Never sum the leaderboard to drive a stat card; it under-counts for programs with more than a page of referrers.
4. Attribution carriers: the ref_code param
Two carriers move the code from click to signup:
?ref_code=<code>query parameter — the primary carrier, appended by the/r/:coderedirect. It survives a jump to your external signup page, where the cookie never round-trips back to this origin.ref_codecookie (HttpOnly, 30-day TTL) — the redundancy carrier. It protects attribution when your landing page strips query parameters, and it carries the code, program id, and tenant schema.
ref_code (or short alias ref) query parameter first, falls back to the ref_code cookie, and calls the referral conversion logic with the new contact id plus the referee’s IP and user agent. There is no separate “record a referral” endpoint — conversion is a side effect of creating the contact with the referral context present. No dedicated webhook exists either; the standard contact.created event fires, and GET /programs/{id}/conversions is the audit-grade enumeration when you need the linkage explicitly.
5. Abuses and failsafes: caps and approved payouts
A referral program is a public offer with a real reward behind it, so the model fails safe at three points:- Program caps. Bound the program’s life with
start_at/end_atand disable it when a promotion ends, so an old shared link does not keep converting. Credits draw from your wallet balance, so a runaway program spends your budget rather than Orbit’s. - Approved payouts. No conversion pays out on its own.
POST /programs/{id}/conversions/{conversionId}/pay(thepayRewardhandler) is the only path frompendingtopaid, it is role-gated, and the update is scoped by program id — a conversion that belongs to another program returns404, so a payout action can never fulfill the wrong program’s conversion. Every payout is audit-logged with referrer, referee, reward type, and fulfillment summary; the minted discount-code string itself is kept out of the audit trail. - Fraud rejects and rate limits. Same-IP visit+signup, rapid-fire referrer/referee creation, and self-refer attempts are recorded as
rejectedwith a machine-readable reason rather than paid. The public/r/:coderedirect rides the same family of per-IP limits as the other public surfaces (see Rate-limit and cooldown taxonomy), and distributing links over SMS or email still passes the normal send gates — the referral mechanic never bypasses the compliance surface (see Send Gates).
6. Which product surfaces issue on referrals: the incentives engine
Referral payouts do not mint rewards beside the reward rails — they ride the shared Incentives engine, the same fulfillment machinery behind surveys and loyalty redemption. The engine routes each reward to a provider (orbit_code for discount codes, orbit_manual for custom payouts, or a connected external gift-card network), resolves it to a lifecycle status (issued, manual, pending/delivered/failed), and appends it to the tenant’s append-only incentive ledger. The pay response carries a fulfillments array with each reward’s provider and delivery status, and the ledger query GET /api/v1/incentives/issued?source=referral is the audit view over every referral-sourced reward you have handed out. The one exception is credit reward types, which the engine refuses and the billing ledger handles directly.
Route outline
All routes are mounted at/api/v1/referrals behind auth; the public /r/:code redirect mounts separately at the app root so it bypasses auth. Writes are gated to the owner, admin, or developer role.
Cross-links
- Referral programs guide — the operator workflow end to end.
- Public pixels, redirects, and short links — the public surface of
/r/:codeat the pixel level. - Incentives — the reward engine payouts route through.
- Rate-limit and cooldown taxonomy — the limiter family the redirect shares.
- Send Gates — the compliance surface link distribution never bypasses.
- Referrals API reference — per-endpoint schemas and envelopes.