Billing and commission

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:

  1. referral_code on the provision body, if you sent one;
  2. your partner record's default_referral_slug, if the operator set one;
  3. 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. :::