> ## 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: create a program, issue codes, track conversions, and pay rewards

> Run a referral program for your own end-users — define the reward, issue a unique code per referrer, track clicks and signups through the /r/:code redirect, rank a leaderboard, and approve payouts through the incentives engine.

# Referral programs

This guide walks an operator through building a referral program end to end: defining the program and its reward, handing each referrer a unique code, tracking visitors and signups through the public redirect, reading the leaderboard, and approving payouts. It covers the workflow; the [Referrals API reference](/api-reference/referrals) has the per-endpoint contract.

## 1. Mental model

Referral programs on Devotel Orbit solve for a customer of Orbit — **you** — running a referral program for **your own end-users** (your contacts). This is not Orbit's own referral scheme, and it is not the same surface as the [Incentives](/concepts/incentives) engine, which referral payouts route through when the reward is a code or a non-credit perk.

The moving parts:

* **Program** — the offer: what each side earns, when the program is live, and where referred visitors land.
* **Code** — one unique code per referrer contact, minted on demand. Each code carries its own counters (visits, signups, rewards paid).
* **Redirect** — `GET /r/:code` is a public, unauthenticated shortlink that records the visit and sends the prospect to your landing page with the code attached.
* **Conversion** — when a new contact is created carrying a referral code, Orbit records a conversion under the program with a reward status of `pending`, `paid`, or `rejected`.
* **Payout** — an operator approves a pending conversion, which pays the referrer and, on double-sided programs, the referee too.

Three actors, three surfaces: the **referrer** (one of your contacts) shares their link, the **referee** (the new contact) clicks it and signs up, and **you** — the tenant operator — define, watch, and pay out the program. Everything lives inside your tenant; nothing crosses between workspaces.

## 2. Create a program

Create a program once with `POST /api/v1/referrals/programs`:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/referrals/programs" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Q3 customer referral",
    "reward_type": "discount_code",
    "reward_value": "20% off next month",
    "referrer_reward": { "amount": 20, "providerId": "orbit_code" },
    "referee_reward": { "amount": 10, "providerId": "orbit_code" },
    "target_url": "https://your-product.example/signup",
    "start_at": "2026-09-01T00:00:00Z",
    "end_at": "2026-09-30T23:59:59Z"
  }'
```

Key fields on create:

| Field                 | Notes                                                                                                                                                                                                                   |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reward_type`         | `credit` (paid into your wallet balance when you mark the conversion paid), `discount_code` (a redeemable code minted and returned on payout), or `custom` (a tracked payout you fulfill yourself).                     |
| `reward_value`        | Human-readable label for the reward — rendered in the dashboard and shared with referrers.                                                                                                                              |
| `referrer_reward`     | Per-side config: `amount` (minor units for `credit`, percentage for `discount_code`), optional `currency`, optional `providerId` to pin a fulfillment provider, and freeform `metadata` that reaches your own webhooks. |
| `referee_reward`      | Same shape as `referrer_reward`. Omit it for a single-sided program; set it and the referee is rewarded too when you pay.                                                                                               |
| `enabled`             | Defaults to `true`. Set `false` to stage a program you have not launched yet.                                                                                                                                           |
| `target_url`          | Where referred visitors land after the redirect. Must be an absolute `https` URL, 2048 characters or fewer; blank falls back to the Orbit app root.                                                                     |
| `start_at` / `end_at` | Optional bounding window. Referrals outside the window are still visited but come back with `program_not_started` / `program_ended` reject reasons instead of a conversion.                                             |

Two prerequisites to keep in mind:

* **Writes need the owner, admin, or developer role.** Program create, update, delete, and payout approval are all write-gated; reads (list, leaderboard, conversions) only need authentication. The dashboard surfaces the same guard: the Referrals screen and the pay button are role-gated to owner, admin, and billing.
* **Credits draw from your own wallet.** A `credit` reward's payout is a balance credit on your tenant billing account — the referrer and referee both receive it out of your balance, not Orbit's. Budget accordingly if you run a high-volume program.

Programs are created enabled unless you pass `enabled: false`. Update any field with `PUT /api/v1/referrals/programs/{id}` using a partial body (send only what changes). Deleting a program (`DELETE /api/v1/referrals/programs/{id}`) removes its issued codes and recorded conversions, so prefer disabling (`enabled: false`) when the program might come back.

## 3. Issue a per-referrer code

Every contact who refers gets their own code:

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

Issue the code when the referrer first asks for it — the endpoint is mint-or-get, so calling it again for the same contact and program returns the existing code instead of a duplicate. The response carries the code string (8 characters, base58-alphabet without ambiguous glyphs), which you multiply into the share link:

```
https://api.orbit.devotel.io/r/AbCdEf23
```

The shortlink is short enough for SMS and stable for the life of the program. Each code row tracks its own `visits`, `signups`, and `rewards_paid` counters, so a broken channel (an email that is not getting opens) and a strong one (an SMS push that converts) show up separately on the leaderboard.

## 4. Track conversions

The funnel from shared link to recorded signup has two halves:

**The click.** `GET /r/:code` is a public, unauthenticated redirect mounted at the API root — no `/api` prefix, no auth — because the person clicking is a prospect, not an operator. On each click it:

1. Validates the code shape and resolves it to a tenant and a program.
2. Records the visit (bumps `visits` on the code).
3. Sets an `HttpOnly` cookie (`ref_code`, 30-day TTL) carrying the code, program id, and schema so attribution survives even when the landing page strips query parameters.
4. Redirects to the program's `target_url` with `?ref_code=<code>` appended — the query parameter is the primary attribution carrier because it survives a jump to your own external signup page, where the cookie never round-trips back to this origin.

Send the prospect's browser anywhere and the code travels with them. The handler also answers browsers with a small branded error page (and API callers with the standard JSON envelope) when the code is unknown, mistyped, or expired — so a broken link never reads as a raw server error.

**The conversion.** Your signup flow creates the new contact through the normal `POST /api/v1/contacts` endpoint. The create-contact handler reads the `ref_code` (or `ref`) query parameter first, then falls back to the `ref_code` cookie, and calls the referral conversion logic with the new contact id, the referrer's IP and user agent, and the code. No separate "record a referral" endpoint is needed — conversion happens as a side effect of creating the contact with the referral context present.

A successful conversion lands in the program as a row with `reward_status: "pending"`. Several fraud guards run under the hood — two referral-tracking rejections to know about: same-IP referrer/referee (visit and signup from the same IP), and rapid-fire contact creation (referrer and referee created within the same window). Rejected rows are recorded with a `reject_reason` so a suspicious burst shows up in the conversions list rather than silently disappearing; they are not paid out.

Webhook-wise, there is no dedicated referral event — a `contact.created` webhook fires for the new contact just as it does for any other signup. When you need the referral linkage explicitly (which contact came through which program), poll `GET /api/v1/referrals/programs/{id}/conversions` — it is the audit-grade enumeration of every recorded conversion with referrer and referee contact ids.

## 5. Read the leaderboard

The leaderboard ranks the program's codes by conversions:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/referrals/programs/rpg_.../leaderboard?limit=25" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Each row is a referrer code with its `visits`, `signups`, and `rewards_paid` counters and the referrer's human-readable label alongside the raw contact id. Two usage patterns:

* **Leaderboard page size is a cap, not a guarantee.** `limit` defaults to 25 and maxes at 100; a program with more than 100 referrers has a leaderboard that under-counts. For "total signups across the whole program," always use `GET /programs/{id}/totals`, which aggregates every code with no pagination.
* **Payout eligibility is separate from leaderboard rank.** A converted referee does not automatically become payable — a pending conversion is approved by an operator. The leaderboard answers "who is driving signups"; the conversions list answers "whose reward do I pay next."

List conversions for a program with `GET /api/v1/referrals/programs/{id}/conversions` — the endpoint supports `status` (`pending`, `paid`, `rejected`), `limit` (1–100, default 25), and `offset`, and returns the total count alongside the window so you can page through the full set.

## 6. Approve and pay a reward

Payment is an operator decision, not an automatic side effect of the conversion — and the screen is where it happens:

1. In the dashboard, open **Marketing → Referrals**, pick the program, and scroll to the conversions table.
2. Find the pending conversion and click **Pay** (the button is role-gated to owner, admin, and billing — the same guard as the page itself).
3. The UI calls `POST /api/v1/referrals/programs/{id}/conversions/{conversionId}/pay`, the conversion flips to `paid`, and the reward fulfills.

The payout path depends on the program's `reward_type`:

* **`credit`** — Orbit adds the configured `referrer_reward.amount` (and `referee_reward.amount` if set) as wallet credits on your tenant's billing account, one per side. The conversion is marked paid; a failed credit write is logged and captured, but the status change stands.
* **`discount_code`** — the incentive engine mints a unique redeemable code and returns it on the pay response; that code is what the referrer actually receives. The returned conversion carries a `fulfillments` array with each reward's provider and delivery status.
* **`custom`** — a structured payout record (status `manual`, reason `awaiting_tenant_fulfillment`) is issued through the same incentives engine, so a tracked record exists for whatever human process fulfills the reward (gift box, invoice credit, service hour).

The pay endpoint is scoped by program id: if the conversion belongs to another program the request returns `404`, so a payout action can never accidentally fulfill the wrong program's conversion. Every payout is also audit-logged with the referrer, the referee, the reward type, and the fulfillment summary — but the minted discount-code string itself is kept out of the audit trail.

The fulfillment runs through the same engine that powers the [Incentives concept](/concepts/incentives): reward types, provider routing, and ledger accounting are the shared machinery behind every code mint and custom-payout record a referral pay produces. If you need to inspect every payout your program has ever made, the incentives ledger filters by `source: referral` via `GET /api/v1/incentives/issued`.

## 7. Work the dashboard surface

Under **Marketing → Referrals** you get two screens that mirror the API:

* **List view** (`/marketing/referrals`) shows every program with its reward type, reward value, referral window, and enabled state. **Create program** opens a dialog with the same fields as the API body (name, reward type, reward label, per-side reward configs, optional landing URL, optional window). Programs can be deleted from the list after a confirmation prompt — that cascade-removes their codes and conversions, so prefer a disable toggle for dormant programs.
* **Detail view** (`/marketing/referrals/[id]`) drills into one program: the stat cards pull from `/totals` (distinct referrers, total signups, total paid rewards — authoritative over the whole program, not the leaderboard's capped page), the leaderboard table ranks referrers by conversions, and the conversions table lists each signup with its reward status and the **Pay** button gated behind a confirmation dialog.

Program state changes are all driven from the dashboard — disabling a program stops new conversions without losing existing codes, and re-enabling resumes them.

## 8. Guard against abuse and timing

A referral program is a public-facing offer with a real reward attached, so the same budget-safety instincts that apply to a campaign apply here.

* **The fraud guards run automatically.** Same-IP visit + signup, explosive referrer/referee creation within minutes of each other, and self-refer attempts are all rejected with a structured `reject_reason`. An unusually high rejection rate on a program is a strong signal of link scraping or automated abuse — pull it from the conversions list with `status=rejected`.
* **Per-link rate limiting is built in.** The `/r/:code` redirect is rate-limited per client IP (120 requests per minute) so a shared link circulated through a scraper farm does not cost you a page of fake visits; the limiter is the same family as the request caps described in [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy).
* **The send surface stays gated.** If you distribute referral links over SMS or email, the normal send gates — quiet hours, suppression, frequency caps — still apply before the message leaves Orbit. The referral mechanic does not bypass the compliance surface; see [Send Gates](/compliance/send-gates) for what fires on a send attempt.
* **Bounded windows are your friend.** Setting `start_at` and `end_at` on a program keeps rewards scoped to a launch or a quarter, so an old shared link does not keep converting weeks after a promotion ends.

## 9. Worked example: referral for a SaaS onboarding flow

A B2B SaaS — "Acme CRM" — wants existing customers to invite peers. The flow:

1. **Create the program.** Double-sided: the referrer gets a `$50` invoice credit, the referee gets `$20` off their first month — both fulfilled in Acme's own billing, so the reward type is `custom` with the manual-fulfillment provider pinned.

   ```bash theme={null}
   curl -X POST "https://api.orbit.devotel.io/api/v1/referrals/programs" \
     -H "X-API-Key: $ORBIT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "Acme customer referral — Q3",
       "reward_type": "custom",
       "reward_value": "$50 / $20 invoice credit",
       "referrer_reward": { "amount": 50, "currency": "USD", "providerId": "orbit_manual" },
       "referee_reward": { "amount": 20, "currency": "USD", "providerId": "orbit_manual" },
       "target_url": "https://acme.example/signup?plan=standard"
     }'
   ```

2. **Issue codes for the promotion cohort.** Every paying customer gets their own share link generated from their contact id, which drops into a "Give $20, get $50" block inside the product:

   ```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_acme_customer_001" }'
   ```

3. **Track the funnel.** Referred visitors click, hit the Acme signup page carrying `?ref_code=AbCdEf23`, and register. Acme's signup handler calls the standard `POST /api/v1/contacts` with the referral context in place; Orbit records a `pending` conversion on the program with the referrer and referee identified.

4. **Watch the leaderboard.** Read `/leaderboard` to see which customers are driving signups, `/totals` for the all-program aggregate, and `/conversions?status=pending` for the queue of rewards awaiting approval.

5. **Approve payouts weekly.** A teammate opens Marketing → Referrals → the program, confirms the new conversions against a fraud-governed checklist (low same-IP rate, sensible signup timing), and clicks **Pay** on each pending conversion. The reward records flow through the incentives engine, and the weekly audit review pulls the same list from `GET /incentives/issued?source=referral` for whatever needs to be reconciled.

The same pattern maps to any referral-where-a-customer-invites-a-lead motion — from a book-a-demo SaaS to a gym's member-referral promo — without rebuilding the funnel.

## 10. Related reading

* [Referrals API reference](/api-reference/referrals) — per-endpoint schemas and response envelopes.
* [Incentives](/concepts/incentives) — the reward engine referrals route through for codes and non-credit payouts.
* [Rate-limit and cooldown taxonomy](/concepts/rate-limit-and-cooldown-taxonomy) — every limiter family that can fire on a send or a redirect.
* [Send Gates](/compliance/send-gates) — compliance gates applied before an outbound send.
* [Campaigns end to end](/guides/campaign-end-to-end) — referral links are often distributed inside campaigns.
* [Best practices](/guides/best-practices) — audience hygiene and send-cadence patterns that keep a referral program honest.
