Skip to main content

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 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 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).
  • RedirectGET /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:
Key fields on create: 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:
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:
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:
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: 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.
  • 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 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.
  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,get20, get 50” block inside the product:
  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.