Now with MCP · your agent can query commission, affiliates, and payouts. Read-only. Connect a client
← Blog

Why Affiliate Commissions Don't Match Stripe

Your affiliate dashboard and your billing dashboard will never agree to the cent. Here is where the gap comes from, and a 20 minute reconciliation routine.

SSled13 min read
·Share on X

Every founder does the same thing in month one. They open Stripe, see a revenue number, open the affiliate dashboard, see a commission number, divide one by the other, and get a percentage that matches nothing. Then they assume tracking is broken.

It usually is not. The two numbers answer different questions. One counts money that entered your account. The other counts money you promised a third party, on a base you chose, for a subset of customers, at a different point in time. A gap is the normal state. A gap you cannot explain line by line is the problem.

Here are the eight places the difference comes from, ordered by how much they cost, plus a reconciliation routine that takes about twenty minutes a month. The examples use Stripe, but the mechanics apply to Polar and Paddle too, and each section names the field involved.

A receipt being printed

TL;DR

  • The dashboards measure different populations. Commission applies only to referred customers, on the base you configured, so it is smaller than total revenue by design.
  • Tax is the largest single gap. Polar and Paddle are merchants of record, so their totals include VAT that never reaches you.
  • Refunds do not arrive in order. A clawback can land before the sale it belongs to, which is why a refunded row sometimes has no sale above it.
  • Duplicate webhook deliveries are normal in any at-least-once system. Deduplicate on the provider order id, never the event id.
  • Reconcile with two exports and three checks, and write down your definition of revenue before an affiliate asks for one.

Two dashboards, two different questions

Stripe answers "how much did customers pay me this month, in total". An affiliate dashboard answers "how much do I owe this affiliate on the customers they brought". The numerator differs, the denominator differs, the timing differs.

Three filters narrow the affiliate number before you compare anything.

Referral narrows it to customers who arrived through a tracked link or code. If 40 percent of your revenue is organic, that revenue has no affiliate attached and appears on one side only.

Base decides whether commission is a percentage of the gross charge or the pre-tax amount. Those produce different numbers on every taxed sale, and both are defensible if the rule is written down and applied consistently.

Status narrows it again. A conversion is pending until you record a payout, then paid. Refunded rows leave the summary but stay visible. One month can show three totals depending on whether you look at all rows, unpaid rows, or rows that will actually be paid.

None of that is a bug. The bug is when two of those filters are applied on one side and not the other.

The field map: what each provider actually sends

Every provider sends several amount fields in the same payload, and they are not interchangeable. This is what the integration reads per event, and what that choice leaves out.

Provider and event Amount used What it excludes Common surprise
Polar, order.paid net_amount Tax, with discounts already applied total_amount in the same payload is higher
Polar, order.created with paid: false Nothing, the event is ignored Everything A pending order never becomes a conversion
Stripe, checkout.session.completed amount_total Nothing, tax sits inside it by default It is not net of Stripe's processing fee
Stripe, invoice.payment_succeeded amount_paid Nothing, but the first invoice is skipped Renewals arrive on a different object than checkout
Paddle, transaction.completed details.totals.subtotal Tax, and Paddle's fee is not subtracted Amounts arrive as strings in minor units
Generic, your own POST Whatever you send Your call entirely Your own bugs land here, with no provider to blame

Two rows deserve a note. Polar's pending row matters because trials and manual review both produce unpaid orders, and counting them creates commissions on money that has not arrived. Paddle's row matters because "59900" is a string, and a naive parse turns it into 599 or drops it.

Polar and Stripe are handled by native signed webhook handlers, so the field choice is already made for you. If you wire up Stripe yourself, the Stripe affiliate program guide covers the same flow from the merchant side.

Tax is the biggest gap, and it is not your revenue

Polar and Paddle are merchants of record. They calculate tax, collect it, and remit it. The number on the receipt includes that tax. The number in your bank account does not.

A 99 dollar plan sold where VAT is 20 percent produces a charge of 118.80 and revenue to you of 99.00. A commission on the charge pays the affiliate on 19.80 you never received. At 25 percent that is 4.95 of loss per sale, and at a few hundred sales it becomes a visible line item.

That is why a correct integration uses the pre-tax field: Polar's net_amount, Paddle's details.totals.subtotal. Both are post-discount and pre-tax, which is the honest base for commission.

Stripe differs because Stripe is not a merchant of record. If Stripe Tax is on with tax-inclusive prices, amount_total includes the tax and your base is inflated. With tax-exclusive prices the tax is added on top, and amount_total still includes it.

Two honest options exist. Pay on the pre-tax subtotal, which is what a buyer calls the price. Or pay on the gross charge, accept the overpayment as a simplicity tax, and say so in the terms. What you cannot do is claim pre-tax in the terms and calculate on gross in the system, because the first affiliate who checks will find it in ten minutes.

Discounts, coupons, and the $0 first order

Discounts create a second gap, and it usually moves in your favor.

Polar's net_amount is subtotal minus discount, before tax. Paddle's subtotal is the same idea. Stripe's amount_total is post-discount. In all three, the affiliate earns on what the customer paid, not on list price. A 50 percent launch coupon halves the commission with no rule needed.

The edge case is the 100 percent discount and the free trial, because both produce a zero-amount order. A zero order pays no commission and, in a naive system, leaves no record at all. Day 14 arrives, the card is charged, the renewal webhook carries no referral metadata, and the affiliate who did the work gets nothing.

That is a revenue leak disguised as rounding. Two workable designs exist: store the affiliate against the subscription id when the trial starts and inherit it on the first paid order, or skip trials entirely and pay from the first charge, accepting that trial-to-paid is where attribution is weakest. The tradeoffs, including keeping the dashboard honest about which rows are actual renewals, are in Affiliate Commissions on Trials and Freemium Plans.

Refunds that arrive before the sale

Provider webhooks are at-least-once and unordered. A refund issued minutes after a sale can be delivered before the order event it refers to, especially when retries reorder the queue. In that window, a system that only looks for an existing conversion finds nothing, logs a no-op, and then records the sale at full commission when the order finally lands.

The fix is a tombstone: record the refunded order id first, then create the sale already marked refunded instead of pending. Nothing is paid, and the history shows both events.

Two consequences follow. A refunded row keeps its original amount, because the status flips rather than the number being edited to zero. That is right for auditability, and it is why summing the amount column differs from summing the commission column of unpaid rows.

Refunds also reference different identifiers than sales. On Stripe a refund may reference the payment intent, the invoice, or the charge, while the sale was recorded against the payment intent. Good integrations try every candidate id. If yours tries one, some clawbacks silently miss. The full anatomy of that failure is in When the Refund Beats the Sale.

Partial refunds are a policy question. Most programs reverse the whole commission when any refund lands, because prorating a clawback costs more in support time than it saves.

Duplicate deliveries and the wrong order id

Retries are the contract, not a failure mode. Any provider promising at-least-once delivery will resend an event occasionally, and a timeout on your side guarantees a duplicate. Without deduplication you pay twice.

The dedupe key has to be stable across deliveries, which means the provider's order identifier and nothing else. Event ids differ between retries in some providers. Timestamps always differ. Session ids are the classic mistake: recording a conversion against the checkout session id looks fine until the refund references the payment intent, finds no match, and leaves the commission in place.

That is why a conversion row is keyed on the order id, unique per program, and why a duplicate delivery is logged as a duplicate rather than an error. It is also why a "missing" conversion is often a row recorded under a different id than the one you searched.

Timing: the month boundary argument

Two clocks run at once: the conversion timestamp is when the webhook was processed, the provider's is when money moved. Around midnight on the last day of the month, they disagree.

Timezone makes it worse. Store in UTC and filter in UTC, or store local dates consistently, but do not mix. A sale at 23:30 on 31 October in Berlin is 21:30 UTC, so a UTC report puts it in October while a local invoice says 1 November. Both are correct, and the affiliate will see only one.

Payout timing is a third clock. A conversion can sit pending for weeks if payouts run monthly with a minimum. The affiliate checks on the 3rd, sees pending, and emails you. The answer is "payouts run on the 15th", written in the terms and repeated in the welcome email, and it is the most common support question in every program.

When you reconcile, use a closed period and the provider's payment date on both sides, and expect the last day to drift.

Currency: when 9900 is not $99

Multi-currency programs break naive arithmetic. If you sell in USD and EUR, a row stores minor units of whichever currency was charged, so 9900 means 99 dollars in one row and 99 euros in another. Summing the column produces a number in no currency at all.

Two policies are defensible. Convert at the provider's rate on the payment date and pay in your home currency, or pay in the sale currency and let the affiliate absorb the difference. The first is fairer and needs a documented rate source. The second is simpler and produces payouts in several currencies, which is its own cost.

What does not work is converting at today's rate when the payout runs three weeks after the sale. The commission then moves because of a central bank decision the affiliate had nothing to do with, and the dashboard and the payout disagree permanently.

Payout rails narrow the options too: a PayPal balance holds several currencies, a bank transfer does not. Paying Affiliates in Another Currency or Country covers which rails support what.

Renewals, proration, and the invoice that counts twice

Subscription businesses generate events that look like duplicates but are not. The first invoice of a subscription arrives on both checkout.session.completed and invoice.payment_succeeded, and counting both pays twice for one sale. Filtering renewals to billing_reason = subscription_cycle is the standard fix, because the first invoice carries a different reason.

Upgrades and downgrades produce proration. A customer upgrades mid-cycle, the provider credits the unused portion, and the affiliate earns on the difference rather than the full new plan. Defensible, but heavy upgrade months show commissions that do not line up with the prices you advertise. Downgrades can produce a negative line, and a negative commission is not something you can pay. Decide whether the credit is absorbed, carried forward, or ignored.

Annual plans add a third shape. A 990 dollar annual charge in January produces one large commission and eleven months of nothing, which makes the program look profitable in January and dead in February. If you would rather smooth it, pay a fraction monthly and accept the extra bookkeeping. Metered products have the same shape with no annual anchor, covered in Affiliate Commissions on Usage-Based Pricing.

A 20 minute reconciliation, every month

You do not need a data team. You need two exports, a closed period, and a short list of explanations.

  1. Export the provider's payments for the period, including tax and fee columns, filtered to paid.
  2. Export the affiliate conversions for the same dates, all statuses.
  3. Match on order id first, then on customer email, because a missing order id is usually a different identifier for the same sale.
  4. Sum three numbers: gross charges, net revenue, commission. Net revenue divided by commission should equal your configured rate within rounding.
  5. List every unmatched row and assign it a cause before closing the month.

A spreadsheet being filled in

Symptom Likely cause Where to look
Commission is lower than revenue times the rate Only referred revenue is commissioned Referred revenue total, not gross revenue
Gap of roughly the tax rate on every sale Commission on a tax-inclusive amount The amount field used by the integration
One sale, two commission rows Checkout and invoice events both counted Dedupe key on the order id
Refunded row with no visible sale Refund arrived before the order webhook Refund tombstones and the webhook log
Sale missing entirely Referral code absent from checkout metadata Checkout creation, not the webhook
Commission on a trial that never converted Zero-amount order counted as a sale Trial handling and the amount threshold
Totals drift by a few rows each month Month boundary and timezone Closed period, UTC timestamps

After the first pass, nearly every row falls into one of two buckets: not referred, or tax. Once those are named, the rest is small enough to handle by hand. If the list stays long, run the broader affiliate program audit before the next payout.

What to tell the affiliate who asks

Affiliates ask because the answer is not in your terms, not because they distrust you. Publish three definitions and the question stops being a negotiation: commission base is the pre-tax amount paid after discounts, payouts run monthly on the 15th with a 20 dollar minimum, and refunded orders are reversed including partial refunds.

Then answer with data instead of reassurance. Naming the order, the amount, the base, and the status resolves almost every case in one message:

Thanks for flagging this. Order 4821 settled at 99.00 before tax, so your commission is 24.75 at 25 percent, pending and included in the 15 December payout. The refund you saw was a different customer, order 4830, and it is already reversed. The row is in your dashboard under Conversions.

That reply ends the thread. "It should be correct on our end" starts a week of messages.

The deeper lesson: reconciliation is a product feature, not an accounting chore. A program that shows amount, base, and status per row, and exports them, spends its support time recruiting. The attribution rules that produce the numbers are in Cookie Windows and Attribution Models Explained.

FAQ

Why is my affiliate dashboard showing less revenue than Stripe? Because it counts only referred customers, often pre-tax, and removes refunded rows from the summary. Total revenue includes organic sales, tax you remit, and refunded charges. The affiliate number should be smaller. If it is larger, look for duplicate order ids.

Does commission get paid on tax? Not with a correct integration. Polar reports net_amount and Paddle reports a pre-tax subtotal, both after discounts, and that is the base. On Stripe it depends on your tax configuration and your terms, which is why the terms should name the base instead of saying "revenue".

Why did a refund show up before the sale it belongs to? Webhooks are delivered at least once and not necessarily in order, so a refund issued minutes after a sale can arrive first. It is recorded as a tombstone and the sale is created already marked refunded. Nothing is paid, and both events stay visible.

Can I check these numbers without asking support? Yes, if the tool exports conversions with the filters you need. In Sled the CSV export mirrors the Conversions page filters, including status, provider, and date range, so you can match rows against a provider export. The webhook log shows why each event was accepted, ignored, or deduplicated.


If the gap turns out to be a clawback rather than a bug, When the Refund Beats the Sale walks through the event order. Sled reads the net amount from Polar, Stripe, and Paddle, keeps refund tombstones, and exports every row for a flat $19/mo with no cut of your commissions. Start the 10-day trial.

Try Sled

Recurring commission on every renewal.

Polar-native affiliate tracking, with Stripe and Paddle handled the same way. 10-day trial, no card required, cancel anytime. Then $19/mo or $99/yr.

Keep reading

All posts →