Money for platforms
You earn an application fee on your accounts’ payments, on top of ours. Your fees collect in your Connect balance, which is swept into your own balance daily and can fund transfers to your accounts. You also bear your accounts’ losses.
Who pays what
Section titled “Who pays what”A £45.00 payment on a UK consumer card (Tuppence’s fee at 0.79%, rounded up),
with your application_fee_amount: 100:
| Account pays our fee (default) | You pay our fee (controller.fees.payer: application) | |
|---|---|---|
Payment amount | 4500 | 4500 |
Payment fee (Tuppence’s) | 36 | 0 (fee_payer: platform) |
application_fee_amount (yours) | 100 | 100 |
| Taken from the payment, as one fee | 136 | 100 |
Account’s net | 4364 | 4400 |
| Into your Connect balance | 100 | 64 (the fee’s tuppence_fee is 36; net is 64) |
A $45.00 payment on a US card (Tuppence’s fee at 2.49% rounded up, plus 20¢),
with your application_fee_amount: 100:
| Account pays our fee (default) | You pay our fee (controller.fees.payer: application) | |
|---|---|---|
Payment amount | 4500 | 4500 |
Payment fee (Tuppence’s) | 133 | 0 (fee_payer: platform) |
application_fee_amount (yours) | 100 | 100 |
| Taken from the payment, as one fee | 233 | 100 |
Account’s net | 4267 | 4400 |
| Into your Connect balance | 100 | -33 (the fee’s tuppence_fee is 133; net is -33) |
fees_payer is set when the account is made and can’t change. If you pay our fee and take none
of your own, your Connect balance goes below zero. Whatever is below zero at month end is debited
from your own balance on the 1st.
Take a fee
Section titled “Take a fee”application_fee_amount (minor units) on POST /v1/payments, POST /v1/payments/charge_saved or
POST /v1/bookings/:id/payments, made as the account.
curl https://api.tuppence.ai/v1/payments \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Tuppence-Account: acct_1…" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 4500, "currency": "gbp", "application_fee_amount": 100}'curl https://api.tuppence.ai/v1/payments \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Tuppence-Account: acct_1…" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 4500, "currency": "usd", "application_fee_amount": 100}'curl https://api.tuppence.ai/v1/payments \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Tuppence-Account: acct_1…" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 4500, "currency": "eur", "application_fee_amount": 100}'const payment = await tuppence.payments.create( { amount: 4500, currency: "gbp", application_fee_amount: 100 }, { tuppenceAccount: "acct_1…" },);const payment = await tuppence.payments.create( { amount: 4500, currency: "usd", application_fee_amount: 100 }, { tuppenceAccount: "acct_1…" },);const payment = await tuppence.payments.create( { amount: 4500, currency: "eur", application_fee_amount: 100 }, { tuppenceAccount: "acct_1…" },);payment = tuppence.payments.create( amount=4500, currency="gbp", application_fee_amount=100, tuppence_account="acct_1…")payment = tuppence.payments.create( amount=4500, currency="usd", application_fee_amount=100, tuppence_account="acct_1…")payment = tuppence.payments.create( amount=4500, currency="eur", application_fee_amount=100, tuppence_account="acct_1…")Or set it up ahead, as the account, so it’s taken without your platform in the loop:
| Where | Field |
|---|---|
Payment link with a fixed amount | application_fee_amount |
| Payment link where the payer chooses | application_fee_percent ("2.5") |
Plan (POST /v1/plans) | application_fee_percent: every charge of a subscription, including its link’s first payment |
The account’s default (POST /v1/accounts/:id) | settings.application_fees: percent (string, "1.5") of the amount rounded up, plus fixed_amount. Applies wherever a payment names no fee |
Fees on your accounts’ stores
Section titled “Fees on your accounts’ stores”A store you run for an account (with Tuppence-Account on the store routes) takes your fee on
every store payment: the web checkout, the Storefront API, hand-offs and agents’ own checkouts.
| Where | Field |
|---|---|
Your default for every account’s store (POST /v1/account) | settings.connect.defaults.store_application_fee: percent and fixed_amount, as above |
One account’s store (POST /v1/accounts/:id) | settings.store.application_fee. Wins over your store default |
A store payment takes the account’s store fee, else your store default, else the account’s general fee. Other payments never read the store fees. Both are held to the cap. A store refund leaves your fee with you unless you give it back.
An order a sales channel took (TikTok Shop, eBay) was paid there, so it carries no fee:
store.order_placed says application_fee: null with application_fee_reason: "paid_on_channel".
How a new account’s store starts: settings.connect.defaults.store on POST /v1/account.
| Field | Default | When false |
|---|---|---|
enabled | true | The store is made with its checkout paused (checkouts_paused.code: "platform"). Open it with POST /v1/sites/:id/resume_checkouts, as the account |
agent_native_checkout | true | AI agents hand the shopper to the store’s checkout instead of paying themselves |
GET /v1/accounts/:id/store reads an account’s store: its address and status (none,
not_open, open, closed), and this month’s store orders, revenue and your fees on them.
The account’s page in the dashboard shows the same under Store.
Your accounts’ stores at your own domain
Section titled “Your accounts’ stores at your own domain”Each account’s store can live under a domain of yours, such as shops.yourplatform.com, instead of
its Tuppence address:
- Set
settings.connect.store_domainonPOST /v1/accountto the domain. - Add the two records
settings.connect.store_domain.recordslists at your DNS host: aTXTrecord at_tuppence.<domain>, which proves the domain is yours, and one wildcardCNAMEfrom*.<domain>to us. - Tuppence looks for the
TXTrecord every ten minutes, or at once when you send the same domain again. Until it is seen,statusispendinganderrornames the record to add; after seven days it isfailed.
Once it is verified, every account’s store gets <its slug>.<domain> (a store made later gets
its own as it is made). Each address is a platform domain in the store’s domains: verified
while its certificate is issued, then active. From then on it is the store’s address: its
Tuppence address redirects there, Apple Pay and Google Pay work there, and the footer reads
Powered by your platform’s name. Each business is still the seller on every page. An account’s
own domain, if it adds one, wins over yours. A new slug moves its address with it, and
store_domain: null takes every address away again.
| Rule | Detail |
|---|---|
Cap: 20% of the payment, rounded down (on £45.01$45.01€45.01, 900) | Over it: 400 application_fee_too_high with max_fee_bps (2000), max_fee_amount and fix_url. Ask for a higher cap in Settings → Platform. A raised cap applies to every account on your platform |
| Cap on fees set up ahead | Refused when you set them, not when a payer pays: a link’s fee or percent, a plan’s percent, an account’s default percent, your platform defaults. A default fixed_amount over the cap on a small payment is taken up to the cap |
Account keeps at least 1 | A fee of the whole amount or more is refused when the payment is made |
| Room for our fee | Our fee is known only once the card is, so a fee that leaves no room for it (and that 1) is refused at confirm with 400 application_fee_too_large. Make a new payment with a smaller fee |
| Partial capture | Takes the same part of your fee, rounded up |
| Your own payment | A fee on a payment not made as a connected account: 400 platform_account_required |
Your fee object
Section titled “Your fee object”Each fee is an application_fee (fee_…), announced with application_fee.created.
GET /v1/application_fees lists them (?account=, ?payment=, ?status=). A fee is pending
until the payment’s money becomes available (available_on), then available, then
swept. A connected account’s balance transactions show both fees in fee_details.
Give it back:
| How | Returns |
|---|---|
refund_application_fee: true on POST /v1/refunds, as the account | Your fee in proportion to the refund, rounded down; the refund that empties the payment returns the rest |
POST /v1/application_fees/:id/refunds, your own key | Some or all (the rest, by default) |
Each is a fee_refund (GET /v1/application_fees/:id/refunds), announced with
application_fee.refunded, and goes back to the account’s balance. Only your fee is returned,
never ours.
Your Connect balance
Section titled “Your Connect balance”See connect on GET /v1/balance, its movements at
GET /v1/balance/transactions?balance=connect, or the dashboard’s Balance page.
- Daily sweep at 05:30 UTC: if the available amount is £1.00$1.00€1.00 or more, it moves
into your own balance in one transfer and those fees become
swept. It’s then paid out with the rest of your money.connect.next_sweepsays when the next sweep runs. - Below zero: a fee given back after it was swept takes the balance below zero. Your next fees make it up before anything more is swept.
- Test mode: a fee on a payment made with
pm_card_bypassPendingis available at once. With an ordinary test card it stays pending for the card’s clearing days. Advancing your test clock runs the sweeps for the skipped days but doesn’t shorten the card’s pending period.
Send money to your accounts
Section titled “Send money to your accounts”POST /v1/transfers, with your own key, sends from the available part of your Connect
balance. It reaches the account’s balance at once, and you both hear transfer.created.
curl https://api.tuppence.ai/v1/transfers \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 500, "currency": "gbp", "destination": "acct_1…", "transfer_group": "referrals"}'curl https://api.tuppence.ai/v1/transfers \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 500, "currency": "usd", "destination": "acct_1…", "transfer_group": "referrals"}'curl https://api.tuppence.ai/v1/transfers \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"amount": 500, "currency": "eur", "destination": "acct_1…", "transfer_group": "referrals"}'const transfer = await tuppence.transfers.create({ amount: 500, currency: "gbp", destination: "acct_1…", transfer_group: "referrals",});const transfer = await tuppence.transfers.create({ amount: 500, currency: "usd", destination: "acct_1…", transfer_group: "referrals",});const transfer = await tuppence.transfers.create({ amount: 500, currency: "eur", destination: "acct_1…", transfer_group: "referrals",});transfer = tuppence.transfers.create( amount=500, currency="gbp", destination="acct_1…", transfer_group="referrals")transfer = tuppence.transfers.create( amount=500, currency="usd", destination="acct_1…", transfer_group="referrals")transfer = tuppence.transfers.create( amount=500, currency="eur", destination="acct_1…", transfer_group="referrals")A transfer is money leaving your platform, so the same checks as your payouts apply. Transfers and payouts share one daily limit and one exposure cap: what you transfer today you can’t pay out, and a reversal gives it back.
| Refusal | When |
|---|---|
402 balance_insufficient (with available) | More than is available. Pending fees fund nothing |
403 payouts_not_approved | Until we’ve reviewed your account |
403 new_account_floor (with available_from) | Your first 168 hours |
403 payouts_on_hold | While your payouts are held |
403 payout_limit (with fix_url) | Above your payout limits |
- The account can pay transferred money out 7 days after it arrives. Until then it’s in its balance but not cleared.
- Take it back:
POST /v1/transfers/:id/reversals(the rest, by default) while the account still holds it; otherwise402 balance_insufficient, naming the account. - List:
GET /v1/transfers(?destination=,?transfer_group=) andGET /v1/transfers/:id/reversals. - Account to account: a reversal, then a transfer.
Top up your accounts’ cards
Section titled “Top up your accounts’ cards”Your connected accounts top up their card balance as any business does: as them, with
Tuppence-Account (or Open as in the dashboard), or on a hosted page you send them. You can
also top one up from your own Connect balance, and set what your accounts may do.
From your Connect balance: POST /v1/issuing/topups with method: platform, as the account.
It is a transfer from the available part of your Connect balance (the same checks and refusals as
POST /v1/transfers) and it’s free.
curl https://api.tuppence.ai/v1/issuing/topups \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Tuppence-Account: acct_1…" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"method": "platform", "amount": 5000}'const topup = await tuppence.issuing.topups.create( { method: "platform", amount: 5000 }, { tuppenceAccount: "acct_1…" },);topup = tuppence.issuing.topups.create( method="platform", amount=5000, tuppence_account="acct_1…")| Region | What happens |
|---|---|
| US | available at once: the account’s cards spend it straight away. Like any transfer, the account can pay it out only 7 days after it arrives |
| UK | It lands in the account’s payments balance, held for the cards, and waits there processing (delay.code: moving_to_card_balance) until the move onto the card balance opens. It is never paid out |
Only you can make it, acting as the account: anyone else gets 400 platform_account_required.
GET /v1/issuing/topups/options lists platform only for you, with what your Connect balance
holds.
A hosted page: POST /v1/issuing/topup_links (as the account, with return_url and an
optional amount) answers a url, shown only then. The page shows your name, logo and colour
(Settings → Branding), takes a card or shows the card balance’s bank details, works once and
lasts 24 hours. In a browser, tuppence.topups.redirect({ url }) from @tuppence/js sends the
person there.
Your settings: settings.connect.card_topups on POST /v1/account, or Platform settings →
Card top-ups.
| Field | What it sets |
|---|---|
enabled | false switches top-ups off for every account (yours from your balance still work). Default true |
methods | The ways your accounts may top up: any of bank_transfer, card, balance, platform. null (the default): all |
platform_fee | Your fee on their card top-ups: rate_bps of what arrives (at most 500, 5%) plus fixed, on top, never more than 5% of the top-up. Always shown to them, and paid into your Connect balance as an application fee with source: issuing_topup |
limits | per_topup and per_30_days, in minor units: lower than Tuppence’s only |
auto_topup_allowed | Whether they may switch on auto top-up. Default true |
Who pays Tuppence’s fee follows the account’s fees.payer, as on its payments. With
application, your Connect balance pays it and the account sees none: a card top-up of 10000
with a 1% fee of yours charges the account 10100, and your Connect balance gets 100 less our
fee. The top-up’s fee says so: paid_by and platform_fee.
When your accounts are paid
Section titled “When your accounts are paid”Daily by default: whatever has cleared under the account’s settlement tier. Change it with
settings.payouts.schedule on POST /v1/accounts/:id.
| Field | Values |
|---|---|
interval | daily; weekly with weekly_anchor (monday…); monthly with monthly_anchor 1–31 (the last day in a shorter month); manual (never paid automatically: you pay it with POST /v1/payouts, as the account) |
delay_days | "minimum" (its settlement tier’s; the default), or a longer number. Shorter than the tier is 400 |
GET /v1/payout_settings, as the account, shows its schedule and next payout. Instant payouts
work as the account too, on the same terms as yours.
When an account goes below zero
Section titled “When an account goes below zero”A refund or lost dispute can take an account below zero. We first try to recover it from the
account’s bank. If it’s still below zero three days later, you cover it in this order. You hear
each step as account.negative_balance; the account has no dashboard and is told nothing.
- Your Connect balance: what’s available goes to the account.
- Your own balance: the rest is debited from it, as your platform agreement allows.
- Neither covers it: your payouts and your accounts’ wait. Tuppence writes the rest off and charges it to your Connect balance, collected on the 1st.
Cashback on your businesses’ cards
Section titled “Cashback on your businesses’ cards”Your accounts’ cards earn cashback on eligible spend: 0.89% in the UK (1% in the US). You earn it and choose how much to pass on.
| Your share | cashback_share: a percentage of card spend, "0" to under "5", at most two decimals |
| Your default | settings.connect.defaults.cashback_share on POST /v1/account, or Settings → Platform |
| One business | settings.cashback_share on POST /v1/accounts/:id; null returns to your default |
| Nothing set | Passes on nothing: you keep it all. You can pass on all of it ("0.89") or more; any excess comes from your Connect balance |
| Which share applies | Read at statement time: the business’s own, else your default, else 0. A change applies from the next statement |
| Refunds | Shares are worked out on the month’s total spend, rounded down. A refund in a later month is taken back from both sides on the next statement |
| Balance can’t cover a share | The share waits and you get an alert. Once covered, the daily sweep sends it first and holds it back from what it sweeps. Your balance never goes below zero for a share |
Payment. Each business gets a monthly statement, made early the next month and checked by our team. When it’s paid:
- what you earned (
net) goes into your Connect balance; - the business’s share (
share_net) goes from there to its payments balance, as a transfer withtransfer_group: cashback_<month>.
The statement, read as the business (GET /v1/cashback/statements), names you as beneficiary
with share_bps, share_gross, share_reversals, share_net and share_transfer_id.
GET /v1/cashback/connected_accounts?period=2026-10 lists each business’s month: card spend, what
you earn, pass on and keep. The dashboard shows the same on Cashback and each business’s
Cards and cashback.
Your statements
Section titled “Your statements”Your monthly statement (GET /v1/statements/:month, or Reports → Monthly statements):
| Statement | Lines |
|---|---|
Yours, connect section | Opening and closing balance; your fees and fees given back; our fees you paid; transfers to accounts and reversals; swept into your balance; accounts’ losses; fees per account (by_account) |
| Yours, own balance | connect_swept (swept in), connect_losses (losses debited from your own balance) |
| Each account’s (read as it) | application_fees (your fee), transfers_in, and processing_fees_paid_by_platform if you pay our fee (it never touched the account’s balance) |
- Webhooks for platforms: hear fees, transfers and losses
- Testing Connect: move test money at once
- Pricing