?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.
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:
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.
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_idcan 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 anameor anemail.
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.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 atGET /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.
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:
@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.