Skip to main content
A referral program is three facts and one rule that runs through all of them. A code belongs to one of your customers and carries what it pays. An attribution says one contact came in through one code — once, forever. A commission is a line in a ledger, written when an invoice is paid and written again, negative, when it is refunded. The rule is first touch: whoever referred first referred. A second ?ref= never re-points somebody, because the commission was already earned by the first one. The panel draws it under Revenue → Referrals, behind billing:read and billing:write.
Nothing here pays anybody. This records what is owed; the transfer is your act, in your own payment gateway, under your own terms. Every response says so with payouts_are_manual: true, and the screens repeat it beside the number — a total that did not say it would be read as one that had already been paid.

Issue a code

A code belongs to a contact — the person who will be paid for the introduction — and it carries its terms. There is no environment-wide default to fall back on: a code with no terms is a payout nobody agreed to.
Exactly one of the two terms: The environment travels as ?environment=live or ?environment=test on writes too, and it is not optional — a code issued with no environment would have to default to one, and defaulting to live is how a code meant for a staging run starts attributing real sign-ups. Turning a code off and back on is two routes rather than one field, because they are opposite acts:
An inactive code attributes nobody new. What it already brought in keeps earning: the attribution was a fact about the past, and deactivating is a statement about the future.

Capture the click

There is no endpoint for this. ?ref=CODE is read off the landing page by the SDK, stored with the rest of the first-touch attribution, and rides along with whichever identifying call happens first — a boot with an external_id, or a redeemed magic link.
That is the whole integration, and it is why it works for somebody who clicks on Monday and signs up on Thursday: the value is held locally and survives the days between the click and the account. A dedicated endpoint would be a second path to the same state — and the one that forgets the rules below.
The code is captured with the rest of first touch, never on its own. If you strip ?ref= from the URL before the SDK runs, or your landing page redirects without carrying the query string, nothing is captured and there is no second chance: first touch happened, and it recorded nothing.

What attributes nothing, and never fails a sign-up

Every one of these writes no attribution and refuses nothing — the sign-up that carried it succeeds either way:
  • A code that does not exist in this environment: a typo, or a link built by hand against the other environment.
  • An inactive code.
  • Somebody’s own code. The account that pays would be the account that gets paid, which is a discount pretending to be a referral.
  • A second ?ref= for somebody already attributed. First touch stays.
  • An anonymous visitor. Attribution needs an identified contact; a device that never signed in is a row nobody could ever find again.
  • An unproven federated boot for a contact that already exists. A hashless boot is an assertion anybody who knows an external_id can make from a browser, so letting one attach a referral to an existing person would be a commission stolen with a query parameter. A boot that creates the contact may attribute it — the same rule that stops an unproven call rewriting a name or an email.

What a payment earns

When an invoice of a referred customer is paid, a commission line is written. It is derived from what has actually stayed paid — the invoice total minus everything refunded — so a refund is not a separate path but the same calculation over a smaller number. For a share: net × rate_basis_points ÷ 10000, truncated, so you never pay a fraction of a cent above the terms. For a flat bounty it is proportional to what stayed paid, which is the one arithmetic decision here worth stating. All-or-nothing would mean a one-cent goodwill refund erases a R$ 50 bounty; “keep the whole bounty unless fully refunded” would pay it in full on a 99% refund. Proportional answers both. An over-refund — a credit larger than the invoice — takes back the commission and no more. A negative balance would be an affiliate owing you money for a sale they made.
A flat-fee code meeting an invoice in another currency records nothing and is logged: no exchange rate is invented on a payout. Either the terms or the price is wrong, and which one is a decision only you can make.

Read what you owe

Balances first, the lines behind them second, in one answer — the report is a screen somebody opens to decide who to pay this month, and the lines are the evidence for the number beside each name.
Balances are per code and per currency, and never summed across currencies — two prices in two currencies are two amounts somebody agreed to, not conversions of one another. Each line’s kind is earned or reversed, and basis_minor is the amount the commission was calculated on. entries is paginated with limit (50 by default, 200 at most) and offset; balances is not, because it is one row per code and currency. The ledger is append-only: a correction is a new line, never an edit, and the database refuses an UPDATE outright. Which is also why the balance is a fold over the lines rather than a stored total that could disagree with them.

What the referrer sees

Your customer reads their own side at GET /v1/contact/referrals, with their contact session — the codes they hold, how many people each brought, and what each has earned. The hosted portal draws it as Indique. It needs a verified contact session, unlike most of /v1/contact/*. The answer is money owed to whoever is holding the session, so an identified session that proved nothing would let anybody who knows an external_id read somebody else’s earnings from a browser. An anonymous session passes and reads an empty list, which is true rather than a refusal: a visitor holds no codes.
It never names who was referred. That is a list of other people’s accounts, and the person holding the code has no claim on it — the count is what they are owed an answer about. There is also no route for a contact to mint their own code. One anybody can mint for themselves is one an attacker mints in a loop; codes are issued by you.

In your own app

<Referrals /> draws the same screen inside your product, and client.getReferrals() hands back the raw answer for anyone building their own:
In a Next app, import it from @userkit/nextjs — the same component, and in proxy mode the read is forwarded by the handlers, with the session in an httpOnly cookie on your own origin. It carries no X-Customer-Id: the commission belongs to the contact, and switching teams does not move it. The component carries the route’s three refusals: it never lists who was referred, never offers to mint a code, and renders payouts_are_manual as a sentence rather than a button nothing behind it could honour. It draws nothing for somebody who holds no code — most people never will, and a box explaining that to all of them is noise. empty is your page’s own sentence for the other case: who gets a code, and how, is your policy, and no dictionary here can guess it.

What survives what

Two questions come up once the program has been running a while, and the answers are deliberate rather than incidental:
  • A merge. When two contacts are merged and both carry an attribution, the earlier one survives — first touch applied by comparing, because only one attribution can live on the target.
  • An erasure. The commission lines outlive the contact. Money owed to the person who made the introduction does not stop being owed when the person they introduced closes their account, so the lines carry the ids by value and no foreign key drags them away.