Your customers pay list price. You never mark up. That is the commercial model, and it is not a policy statement — nothing in this system resolves a per-partner price or a per-partner payer, so there is no mechanism through which you could mark up. What you get instead is a rate, and two ways to take it.
| Who pays us | What you get | When | |
|---|---|---|---|
| A. Your customer buys from us | the customer, at list price | commission, paid into your funding account | after a hold |
| B. You buy on their behalf | you, at your rate off | a discount at the till | immediately |
Both net us the same money, which is why you may use whichever suits your product. At the 20% rate: we take $100 and pay you $20, or we take $80 and pay you nothing. Either way we net $80 and your customer receives $100 of balance.
Path A does not require that you provisioned the account. It covers any account attributed to you, including one that arrived through your referral link and signed up on our own website — see the two doors.
::: danger Discount or commission on the same dollar. Never both.
This is enforced in code, in two places that must agree. Money that reached a customer's wallet at
your expense is not commissionable — the discount you already took was the commission. If that rule
were ever relaxed, path B refuses to move a cent rather than completing one half of a double payment
(503 double_pay_guard, which should never fire).
:::
Your rate
20%, flat, for as long as the customer stays. It does not climb, it does not expire, and it is the same number in both paths — read from one place, so the discount that comes off a price is exactly the commission that would have been paid instead.
There is no rate ladder and no earning window. You are paid the same percentage on a customer's first dollar and their four-thousandth, whether that is next month or in three years.
A negotiated override beats the flat rate, which is the escape hatch if your volume outgrows it — ask. Every rate, flat or negotiated, is clamped at 50%: break-even, because above it a heavy customer is sold at a loss and heavy customers are precisely the ones a partner is paid to bring.
::: info Your own referral link is on these terms too.
You do not have to provision an account to earn on it. If you send someone to fluidvip.com through
your referral link and they sign up themselves, they are still your customer commercially: 20%, for
life, on everything they ever buy. Whether you created the account or they did changes nothing about
what you are paid.
:::
Do not hardcode any of these. They are served, and a UI that disagrees with the server about what you earn is worse than a UI that shows nothing:
curl https://fluidvip.com/api/me/partner-program \
-H "Authorization: Bearer <your Fluid session token>"
The config object on that response carries the live rate, the hold, the ceiling and the payout
minimums.
Path A: your customer buys from us
Your customer pays list price on our site — a wallet top-up or a subscription — and you are paid commission on it.
What is commissionable: wallet top-ups and subscriptions. Under the current model a top-up dollar is exactly as paid as a subscription dollar.
What is not: anything that reached the wallet at your own expense (a purchase or a seed), and metered usage. Commission on the markup inside a usage charge is no longer accrued — that writer was removed when commission moved from usage to purchase. Rows already written are money records and are still summed into your balance, so a long-standing partner may see a small usage figure that never grows.
| Value | |
|---|---|
| Hold before a commission can be withdrawn | 30 days |
| How long a referred account keeps earning | for as long as it stays a customer. There is no expiry |
| Minimum cash payout | $100 |
| Minimum balance payout | $10 |
| Uplift for taking earnings as Fluid balance | +20% |
The 30-day hold covers the 14-day EU withdrawal right in our refunds policy, with margin.
Payouts are requested on the Affiliate tab of the same account — not from the partner panel and not from any partner API. There is deliberately only one implementation of the minimums.
::: tip Your earnings will read $0.00 for a while if you only look at the rounded figure.
The panel returns money twice: earnings in cents (the authority a payout validates against) and
earnings_precise un-rounded. A partner earning small amounts has a genuinely growing balance that
rounds to zero for weeks. Read earnings_precise when you display, earnings when you pay.
:::
The two doors a customer can arrive through
Commission is owed on any account attributed to you. There are two ways an account becomes attributed, they pay identically, and neither is preferred:
| Door | How it is attributed | Your customer |
|---|---|---|
| You provision it | on the POST /partner/accounts call — see below |
never visits our site; may never hear our name |
| Your referral link | when they first load their account after signing up | signs up at fluidvip.com themselves, through your link |
The second door is the one to know about if you have an audience as well as a product. Share your link, and anyone who follows it and buys is yours on the same 20%, for life — you did not provision the account, you do not operate it, and it makes no difference to what you are paid.
Your link lives on the Affiliate tab of your funding account, beside your earnings and the payout button. You can create more than one and point each at whichever product you actually talked about, so you can tell which channel is working.
::: warning Opening your own link while signed in will say self_referral. That is correct.
Nobody earns commission on their own money, so your own account cannot be attributed to you. It is
also the first thing every new partner does to check the link works, so: the refusal means the link
resolved, which is the thing you were testing. Open it in a signed-out browser to see what a customer
sees.
:::
Attribution happens once, and it is write-once
Whichever door, the binding is write-once: first touch wins, across your own links and across partners. On the provisioning door it is attempted on every provision — including idempotent replays, which is what makes a provision that crashed mid-flight safely retryable.
The precedence when provisioning:
referral_codeon the provision body, if you sent one;- your partner record's
default_referral_slug, if the operator set one; - your own referral link, which enrols your funding account on first use.
The referral object on the provision response tells you what happened:
{"referral": {"bound": true, "reason": "bound"}}
An unknown code, a self-referral and an already-bound account are all normal outcomes meaning "carry on, no attribution" — they never fail the request.
::: warning Provisioning refuses outright until you have a funding account. With no funding account there is nobody to attribute an account to, and the binding is write-once — so an account created in that state would stay unattributed permanently, even after the account was linked. Your commission on that customer would be gone with no way to take it back.
That used to be reported as "reason": "partner_not_linked" inside a healthy-looking 201, which
is how one shipped unattributed. It is now a 409 no_funding_account that fires before anything
is written, so nothing exists to be stranded and you can re-send the same external_ref once we
have linked you. See Getting set up.
:::
Path B: you buy on their behalf
POST https://api-account.fluidvip.com/api/partner/accounts/{owner_id}/credit
You are charged your rate off the face value; your customer receives the full face value. A $100 top-up costs you $80 and they still get $100.
| Name | Type | Required | Description |
|---|---|---|---|
amount_usd |
number | Yes | The face value — what the customer receives. Must be > 0 |
reference |
string | Yes | Your own id for this purchase. 1-128 chars matching ^[A-Za-z0-9._:-]+$ |
note |
string | No | Max 200 chars |
There is no field for what you pay. That is resolved server-side from your rate, for the same reason a plan's price is not sent by the caller: a discount the caller names is a discount the caller sets.
curl -X POST \
https://api-account.fluidvip.com/api/partner/accounts/9c1f4b7e-2a58-5d03-b6e1-77af0c9d3218/credit \
-H "X-Partner-Key: fv_pk_…" \
-H "Content-Type: application/json" \
-d '{
"amount_usd": 100,
"reference": "acme-order-55219",
"note": "Studio plan top-up"
}'
201 Created, or 200 OK on an idempotent replay:
{
"id": "3a7f81de-90cb-4e2f-a1d5-6b0c7e2849aa",
"owner_id": "9c1f4b7e-2a58-5d03-b6e1-77af0c9d3218",
"reference": "acme-order-55219",
"face_usd": 100.0,
"charged_usd": 80.0,
"discount_usd": 20.0,
"rate_pct": 20.0,
"note": "Studio plan top-up",
"created_at": "2026-08-25T10:31:09.442117+00:00",
"external_ref": "cust-8842",
"replayed": false,
"partner_balance_usd": 4190.0
}
discount_usd is the gap between face and charged, stated outright rather than left for you to
subtract — that gap is your commission, taken at the till. partner_balance_usd is what is left
in your funding account, so you can warn before the next purchase is refused rather than after.
The charge is face × (10000 − rate_bps) / 10000, rounded to the cent, half-up. Half-cents round
toward us; a discount is a favour and a rounding rule should not quietly extend it.
::: warning reference is required and it is the idempotency key of a money transfer.
Without one, a retried call you never saw the answer to charges you a second time. It is refused
rather than defaulted to a random value, which would look like it worked. A replay returns the
original row with "replayed": true and 200, so you see what you were actually charged rather
than a fresh quote.
A reference differing only by a stray space is a second purchase. Normalise before you send. :::
What can refuse a purchase
| Status | error |
Meaning |
|---|---|---|
402 |
insufficient_partner_balance |
your funding account does not cover the charge. Nothing moved |
400 |
above_ceiling |
over the per-call ceiling, $2,000 by default |
429 |
daily_limit |
over the rolling-24-hour ceiling, $10,000 by default. Operator-owned — you cannot raise it, deliberately, because it exists for the case where your own credential is what went wrong |
409 |
no_funding_account |
your partner record has no funding account linked |
400 |
self_purchase |
the beneficiary is your own funding account |
400 |
zero_amount |
amount_usd was not above zero |
400 |
reference_required |
reference was blank |
404 |
account_not_found |
not an account you provisioned, or its owner revoked you |
503 |
credit_failed |
the transfer could not be completed. Your balance was not charged — the debit is compensated under its own key |
503 |
double_pay_guard |
should never fire. The double-pay rule was relaxed and this path refused to move money rather than pay you twice |
Your seeding budgets do not apply here. Those bound what an automatic program may spend while you are not looking; a purchase is you looking, with an amount in the request.
The starting balance (optional)
You can give every account you provision a starting balance out of your own. It is off by default and every guard fails closed — a refusal is never an error and never fails a provision.
curl -X PATCH https://fluidvip.com/api/me/partner-program/settings \
-H "Authorization: Bearer <your Fluid session token>" \
-H "Content-Type: application/json" \
-d '{
"seed_enabled": true,
"seed_usd": 5,
"seed_total_cap_usd": 500,
"seed_daily_cap_usd": 50
}'
| Field | Rule |
|---|---|
seed_usd |
per account. Capped by the operator at $25 by default |
seed_total_cap_usd |
total exposure. 0 refuses. "Unlimited" is not a value this offers |
seed_daily_cap_usd |
rolling 24 hours. 0 refuses, and it may not exceed the total |
| — | the operator also caps seeded accounts per rolling 24h at 200 |
Refusals on the write, all of which leave the settings row untouched so a rejected PATCH can never
leave seeding half-armed:
| Status | error |
|---|---|
400 |
seed_above_ceiling — names the live ceiling in the message |
400 |
seed_amount_required — enabled with no amount |
400 |
budget_required — enabled with an amount but no budget |
400 |
daily_above_total |
409 |
no_funding_account |
403 |
owner_id_is_operator_only — you may never set the funding account |
A seed is face value both ways: no discount, because nobody is buying anything. And it is one seed per account, ever — enforced by a uniqueness constraint, so retrying a provision moves nothing.
One deliberate behaviour worth knowing: seeding runs on the replay path too. If you turn seeding on
after provisioning some accounts, re-calling POST /partner/accounts with the same references tops
those accounts up once, rather than the setting silently applying only to future accounts.
Reconciling
GET /api/partner/settings (partner key) or GET /api/me/partner-program (your session) both carry
an exposure object, summed from immutable rows rather than kept as a counter — so it cannot drift
from the money that actually moved:
{
"exposure": {
"seeded_total_usd": 145.0,
"seeded_accounts": 29,
"seeded_24h_usd": 25.0,
"seeded_24h_accounts": 5,
"purchased_total_usd": 4200.0,
"purchased_charged_usd": 3360.0,
"purchases": 42,
"purchased_24h_usd": 300.0,
"purchased_24h_charged_usd": 240.0,
"purchases_24h": 3
}
}
purchased_total_usd is face value — what your customers received. purchased_charged_usd is what
you paid. The gap is your commission, taken at the till.
What conversations cost
Provisioning, registration links, account reads and purchases are free — they touch no model.
Conversations are not. Every reply, opener, comment and follow-up is a real call to an upstream
model, metered at exact cost and charged to the character owner's wallet at their plan's rate. That
is unchanged for accounts you provisioned; see Billing & usage for the rate table and
the billing object on every response.
::: danger Nothing is simulated — including in the sandbox. Every conversation spends real money from a real wallet, in both environments. There is no test key prefix, no simulated billing and no dry-run mode anywhere in this system; a sandbox key is an ordinary partner key pointed at a different deployment.
The difference is only whose money. A sandbox partner balance starts at $5 of ours — once,
not per account — and you distribute it to test accounts yourself with the credit call below, which
is the same call your integration makes in production. $5 of yours buys $6.25 of theirs, roughly
several thousand replies at the metered rate. When it runs out, credit answers
402 insufficient_partner_balance and provisioning carries on.
In production the money is yours: treat your integration's model spend as a real line item and keep testing on one account you are watching. :::
Related
- Creating a customer account — where attribution is bound
- Limits and failures — every cap in one place
- Billing & usage — what a conversation actually costs