If your customers pay in their own currency and you pay affiliates in yours, every payment carries two different amounts. Pick the wrong one and every commission you calculate is quietly wrong.
Two amounts, two currencies
Stripe calls the currency the customer is charged in the presentment currency, and the currency that lands in your account the settlement currency. When they differ, Stripe converts, and the conversion is where commission math goes wrong.
The invoice tells you what the customer paid. The balance transaction tells you what you actually received.
// invoice.paid
const invoice = event.data.object;
invoice.currency; // presentment: what the customer was charged in
invoice.amount_paid; // presentment amount, smallest currency unit
// Resolve the charge for this invoice. The field name depends on your API
// version (older versions expose invoice.charge, newer ones invoice.payments),
// so read it from your own version rather than copying a field name.
const charge = await stripe.charges.retrieve(chargeId, {
expand: ['balance_transaction'],
});
const bt = charge.balance_transaction;
bt.currency; // settlement: your account currency
bt.amount; // settled, before Stripe's fee
bt.net; // settled, after Stripe's fee
bt.exchange_rate; // the rate used for THIS payment
Enter fullscreen mode Exit fullscreen mode
Most commission code reaches for invoice.amount_paid, because it is right there in the webhook payload. On a single-currency account that is fine. The moment a customer pays in EUR and your account settles in USD, amount_paid is a number in a currency you never received.
Decide what the commission is a percentage of
There are two defensible answers, and you have to pick one deliberately.
A percentage of what the customer paid. Stable from the affiliate's side: the same sale always earns the same commission in the customer's currency. You absorb the exchange rate movement.
A percentage of what you received. Stable from your side: you never pay out more than a fixed share of money you actually hold. The affiliate's commission moves with the rate, and you have to explain that.
Neither is wrong. Shipping without deciding is, because then the code picks for you, and it picks whichever field was easiest to reach.
The refund trap
This is the part that catches people out.
When a currency-converted payment is refunded or disputed, Stripe converts back at the current rate, not the rate of the original charge. From Stripe's documentation on conversions for disputes and refunds:
Exchange rates fluctuate with the market, so the rate used during the payment can differ from the rate used when a dispute or refund occurs.
Their own worked example: a 60 USD payment at 0.88 EUR per USD settles as 52.80 EUR. If the rate is 0.86 when the refund lands, only 51.60 EUR comes back out of your balance. The customer still gets a full refund in their currency. You carry the difference.
So if you claw back a commission by recomputing it at today's rate, the reversal will not match what you originally recorded. A few hundred refunds later, that is a reconciliation gap nobody can explain.
Store the numbers, not just the percentage
The fix is boring and it holds up. On every commission row, write down enough that you never have to re-derive anything.
create table commission (
id bigserial primary key,
affiliate_id bigint not null,
invoice_id text not null,
-- what the customer paid
presentment_amount bigint not null,
presentment_currency text not null,
-- what you actually received
settled_amount bigint not null,
settled_currency text not null,
exchange_rate numeric,
balance_transaction text,
-- what you owe, frozen at calculation time
commission_amount bigint not null,
commission_currency text not null,
basis text not null, -- 'presentment' or 'settled'
created_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode
basis is the column that saves you. A year from now, when someone asks why two commissions on identical-looking sales came out different, the row says which side of the conversion it was calculated from.
And on a refund, reverse the stored commission_amount. Do not recompute it. The number you owed is the number you un-owe, wherever the rate has moved since.
One sentence for your program terms
If you pay affiliates in their local currency, there is a second conversion between you and them, so the amount landing in their bank will not always match the number in your dashboard. Say that in the terms in one sentence.
It costs nothing to disclose, and it prevents the support email that opens with "your dashboard says 40 and I received 38."
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.