> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Referrals and affiliates

> Issue a code, capture the click, and read what you owe — with the rules that decide who gets credited and what a refund takes back.

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`.

<Note>
  **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.
</Note>

## 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.

```bash theme={null}
curl -X POST "https://api.userkit.dev/v1/organization/referral-codes?environment=live" \
  -H "Authorization: Bearer uk_st_…" \
  -H "X-Organization-Id: …" \
  -H "Content-Type: application/json" \
  -d '{
    "owner_contact_id": "9f2c…",
    "code": "MARIA",
    "rate_basis_points": 2000
  }'
```

Exactly one of the two terms:

| Term                                    | What it means                                                               |
| --------------------------------------- | --------------------------------------------------------------------------- |
| `rate_basis_points`                     | A share of what the referred customer pays. `2000` is 20%.                  |
| `fixed_amount_minor` + `fixed_currency` | A flat bounty, in the currency's minor unit. `5000` + `"BRL"` is R\$ 50,00. |

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:

```bash theme={null}
curl -X POST "https://api.userkit.dev/v1/organization/referral-codes/{id}/deactivate?environment=live" \
  -H "Authorization: Bearer uk_st_…" -H "X-Organization-Id: …"
```

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.

```
https://yourproduct.com/?ref=MARIA
```

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.

<Warning>
  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.
</Warning>

### 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.

<Note>
  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.
</Note>

## 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.

```bash theme={null}
curl "https://api.userkit.dev/v1/organization/referrals/report?environment=live" \
  -H "Authorization: Bearer uk_st_…" \
  -H "X-Organization-Id: …"
```

```json theme={null}
{
  "balances": [
    {
      "referral_code_id": "3d1a…",
      "code": "MARIA",
      "active": true,
      "owner_contact_id": "9f2c…",
      "owner_name": "Maria Andrade",
      "owner_email": "maria@example.com",
      "currency": "BRL",
      "earned_minor": 148000,
      "reversed_minor": -9900,
      "owed_minor": 138100,
      "entry_count": 12
    }
  ],
  "entries": [
    {
      "id": "7be4…",
      "referral_code_id": "3d1a…",
      "code": "MARIA",
      "kind": "earned",
      "amount_minor": 11800,
      "currency": "BRL",
      "basis_minor": 59000,
      "invoice_id": "1f77…",
      "customer_id": "5ca9…",
      "referred_contact_id": "b302…",
      "created_at": "2026-07-31T14:02:11Z"
    }
  ],
  "payouts_are_manual": true
}
```

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.

```json theme={null}
{
  "codes": [
    {
      "id": "3d1a…",
      "code": "MARIA",
      "owner_contact_id": "9f2c…",
      "active": true,
      "referred_count": 12,
      "rate_basis_points": 2000,
      "fixed_amount_minor": null,
      "fixed_currency": null,
      "created_at": "2026-06-02T10:00:00Z",
      "balances": [
        { "currency": "BRL", "earned_minor": 148000, "reversed_minor": -9900, "owed_minor": 138100 }
      ]
    }
  ],
  "payouts_are_manual": true
}
```

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:

```tsx theme={null}
import { Referrals } from "@userkit/react";

export default function ReferralsPage() {
  return <Referrals empty={<p>Ask our team for a referral code.</p>} />;
}
```

In a Next app, import it from `@userkit/nextjs` — the same component, and in
[proxy mode](/en/customer-auth/session-tokens) 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.
