Skip to main content

The referral conversion loop

The Referral programs guide walks the create-and-mint side of referrals: authoring a program, issuing per-contact codes, and the dashboard equivalents in Set up a referral campaign. This guide picks up where those stop, at the moment a referrer actually shares their link. It walks the read side of the loop end to end and shows the response bodies you get back at each step, the part of the API the reference pages name but never illustrate.

1. The loop in one request flow

One referred signup moves through six requests:
  1. Create the program. POST /referrals/programs defines the offer, including the per-side reward config (referrer_reward, referee_reward). Covered in Referral programs.
  2. Mint a code per referrer. POST /referrals/programs/:id/codes returns the contact’s code and its public short link, https://api.orbit.devotel.io/r/AbCdEf23.
  3. The prospect clicks. The public redirect counts the visit and 302s to your landing page with the code attached as a ref_code query parameter (section 2).
  4. Your signup flow passes ref_code on. When your backend creates the new contact in Orbit, it includes the parameter, and Orbit records a conversion (section 3).
  5. You read the funnel. GET /referrals/programs/:id/conversions lists attributed signups; GET /referrals/programs/:id/leaderboard ranks referrers (section 4).
  6. You approve the payout. POST /referrals/programs/:id/conversions/:conversionId/pay flips the conversion to paid and fulfills both sides of the reward (section 5).
Steps 1 and 2 are program setup; everything from step 3 on is the conversion loop this guide covers.

2. Watch the click land

GET /r/:code is public. It needs no API key because it is opened in a prospect’s browser, never by your server. Each hit bumps the code’s visit counter and answers with a redirect:
Two carriers leave this response, and they matter differently:
  • The ref_code query parameter is the one your signup page actually sees. Location points at your own domain, so hold the parameter through your signup form (a hidden field or a query passthrough) and forward it in the create-contact call in section 3. This is the attribution path that works when your signup flow lives outside Orbit.
  • The ref_code cookie is scoped to the Orbit API origin. It only round-trips when a signup never leaves Orbit (a program with no landing URL configured); for an external target_url it never reaches your domain. Treat the query parameter as primary and the cookie as a fallback Orbit reads on its own origin.
When a code cannot resolve (mistyped, deleted, or minted in another environment) a browser receives a small branded “link unavailable” page while an API or SDK caller receives the JSON error envelope, so a shared link never presents a raw server error to a prospect. The redirect is rate-limited per client IP, so a circulated link picked up by a scraper does not inflate your visit counts. Visits need no code on your side: the redirect records them server-side, and they surface per code in the leaderboard below.

3. Confirm the signup attribute

When your backend creates the referred contact, pass the code through with the same ref_code parameter your landing page received. Orbit matches it to the code, records a conversion, and leaves it at pending until you approve the reward:
Read the row as the state of one attributed signup:
  • reward_status moves pending → paid on approval, or straight to rejected when an anti-abuse guard declines it (a signup from the referrer’s own IP, or a referrer and referee created minutes apart). Rejected rows carry a reject_reason and are never payable; the signup still counts toward the referrer’s funnel because the visit and signup genuinely happened.
  • referrer_contact_id / referee_contact_id join the row back to your contacts; referrer_name / referee_name are resolved display labels for rendering directly.
  • converted_at is when the referred contact was created; rewarded_at is filled only when the payout lands.
Filter with status=pending for the approval queue, status=paid for reconciled payouts, and page with limit / offset (max 100 per page).

4. Read the leaderboard

GET /referrals/programs/:id/leaderboard ranks every issued code in the program by signups, then visits:
Three counters per referrer, each with different semantics:
  • visits counts 302s the redirect served for this code (section 2).
  • signups counts every recorded conversion for the code, including rejected ones: a referred signup that a fraud guard declined is still a signup for funnel purposes. Reward eligibility is tracked separately by each conversion’s reward_status.
  • rewards_paid counts conversions that reached paid, incremented only when a payout is approved. It is a count of paid rewards, not an amount; the amounts paid per side come from the program’s reward config (section 5).
The leaderboard caps at 100 rows (limit defaults to 25), ordered so your strongest referrers rank first. For whole-program aggregates (total referrers, total signups, total rewards paid) call GET /referrals/programs/:id/totals instead; that is what the dashboard stat cards read, and it is not capped the way the leaderboard is.

5. Approve the payout end to end

Payouts are an operator decision: a conversion stays pending until you approve it. Approve with the pay endpoint:
The response is your receipt. The conversion flips to paid with a rewarded_at timestamp, the referrer’s rewards_paid counter increments, and the fulfillments array carries one fulfillment record per rewarded side, so a two-sided program returns a referrer entry and a referee entry. What each fulfillment does depends on the program’s reward type:
  • credit applies the configured amount per side, drawn from the reward pool your organization funds. The split comes from the program’s referrer_reward and referee_reward config, so a “10 for the referrer, 5 for the referee” offer pays exactly that on every approval.
  • discount_code mints a redeemable code per side through the incentives engine; the minted code is returned in the response but kept out of durable logs, exactly because it is redeemable. Fulfillment providers for non-credit rewards are configured as described in the Incentives ledger guide.
  • custom records a tracked payout for your own fulfillment process and reports it back in fulfillments so your system has a durable reference to reconcile against.
A payout that fails partway (for example a wallet credit that could not be applied) leaves the conversion marked paid but surfaces the failure in structured logs on the platform side; the fulfillments statuses in the response tell you what actually delivered. Rejected conversions are never payable: the pay endpoint 404s on them, and the reject_reason stays on the row for your review.

6. Idempotency and re-tries

Every write in the loop is safe to replay, by design rather than by header:
  • Minting is mint-or-get. POST /referrals/programs/:id/codes with a contact_id that already holds a code returns the existing code, so a nightly cohort job can call it for every member without checking first, and a retried mint never produces a second link for the same referrer.
  • Attribution deduplicates per referee. A conversion is unique per program and referee contact: if your signup webhook fires twice for the same referred signup, the second create-contact call does not record a second conversion.
  • Pay is pending-only. The pay endpoint transitions pending → paid and nothing else. If your client times out and retries, the retry finds no pending conversion and returns 404 instead of paying twice; the correct move after any pay timeout is to GET the conversions list and read reward_status rather than to re-issue the pay call blindly.
Because these three guarantees live server-side, you do not need client-generated idempotency keys to drive the referral loop safely: design your retry logic around reading state back (conversions, leaderboard) after any uncertain write, and a duplicated request collapses into the same result as a single one.