> ## 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 programs: issuance, attribution, leaderboard, and edge cases

> Tenant-defined referral programs — mint a unique code per contact, attribute the redemption, watch the live leaderboard, fulfill the reward through the incentives engine, and handle duplicate, self-refer, and revocation edge cases.

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

This is the deeper reference behind the [end-to-end Referral programs guide](/guides/referral-programs). 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](/guides/incentives-ledger). 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`.

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/referrals/programs/rpg_.../codes" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contact_id": "con_referrer_abc123" }'
```

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](/concepts/incentives) 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 `pending` → `paid` 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](/guides/incentives-ledger) 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.

## Related reading

* [Referral programs end-to-end guide](/guides/referral-programs) — the operator workflow this page deepens.
* [Incentives ledger](/guides/incentives-ledger) — one-off promos vs. referral-funnel rewards, and the `issued` / `redeemed` / `voided` lifecycle referral rewards fulfill through.
* [Incentives concept](/concepts/incentives) — the shared reward engine behind referrals, surveys, and loyalty.
* [Loyalty program model](/concepts/loyalty-program-model) — the other end-user program that routes redemptions through the same incentives engine.
