> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Referral program model: program → code → conversion → reward

> The referral model behind the public /r/:code redirect and the /api/v1/referrals controller: tenant-owned programs, idempotent per-contact code minting, ref_code attribution, leaderboard and totals aggregation, and the incentives-engine payout path.

# 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](/guides/referral-programs) 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:

| `reward_type`   | What the reward is                                                             | Fulfillment rails                                                       |
| --------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `credit`        | A balance credit on your tenant billing account                                | The billing ledger — the incentive engine refuses to double-pay it      |
| `discount_code` | A unique redeemable code minted on payout                                      | The [Incentives](/concepts/incentives) engine                           |
| `custom`        | A tracked payout you fulfill yourself (gift box, invoice credit, service hour) | A structured `manual` payout record, also through the Incentives engine |

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 with `POST /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](/concepts/public-pixels-redirects) 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:

1. **Program** — defined once, enabled/disabled over its `start_at`–`end_at` window. New conversions are rejected outside the window with `program_not_started` / `program_ended` reasons and disabled programs accept none.
2. **Code issued** — mint-or-get per contact; the code's counters start at zero.
3. **Conversion recorded** — when your signup flow creates a new contact with the `ref_code` query parameter or cookie present, the create-contact handler records a conversion under the program with `reward_status: "pending"`. Fraud guards reject same-IP visit+signup and rapid-fire referrer/referee creation with a structured `reject_reason` (recorded, not silently dropped).
4. **Reward paid** — an operator approves the pending conversion through `POST /programs/{id}/conversions/{conversionId}/pay`, which flips the status to `paid` and fulfills the reward. `rejected` rows never pay out.

Three states exist per conversion (`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/:code` redirect. It survives a jump to your external signup page, where the cookie never round-trips back to this origin.
* **`ref_code` cookie (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.

At conversion time the create-contact handler reads the `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_at` and 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` (the `payReward` handler) is the only path from `pending` to `paid`, it is role-gated, and the update is **scoped by program id** — a conversion that belongs to another program returns `404`, 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 `rejected` with a machine-readable reason rather than paid. The public `/r/:code` redirect rides the same family of per-IP limits as the other public surfaces (see [Rate-limit and cooldown taxonomy](/concepts/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](/compliance/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](/concepts/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.

| Method | Path                                          | Purpose                                                                     |
| ------ | --------------------------------------------- | --------------------------------------------------------------------------- |
| POST   | `/programs`                                   | Create a program — reward type, reward configs, target URL, optional window |
| GET    | `/programs`                                   | List every program (including disabled)                                     |
| GET    | `/programs/count`                             | Return `{ total }` for dashboard badges                                     |
| GET    | `/programs/:id`                               | Fetch one program by id                                                     |
| PUT    | `/programs/:id`                               | Partial update (send only changed fields)                                   |
| DELETE | `/programs/:id`                               | Delete a program, cascading to its codes and conversions                    |
| POST   | `/programs/:id/codes`                         | Mint-or-get the per-contact referral code                                   |
| GET    | `/programs/:id/leaderboard`                   | Top-N codes by conversions (capped page)                                    |
| GET    | `/programs/:id/totals`                        | Aggregate over **every** code — referrers, signups, rewards paid            |
| GET    | `/programs/:id/conversions`                   | Page the conversions with referrer/referee ids and reward status            |
| POST   | `/programs/:id/conversions/:conversionId/pay` | Approve a pending conversion and fulfill its reward                         |

## Cross-links

* [Referral programs guide](/guides/referral-programs) — the operator workflow end to end.
* [Public pixels, redirects, and short links](/concepts/public-pixels-redirects) — the public surface of `/r/:code` at the pixel level.
* [Incentives](/concepts/incentives) — the reward engine payouts route through.
* [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) — the limiter family the redirect shares.
* [Send Gates](/compliance/send-gates) — the compliance surface link distribution never bypasses.
* [Referrals API reference](/api-reference/referrals) — per-endpoint schemas and envelopes.
