Attributing at the invoice, not the click
Click-based tracking knows a sale happened. Invoice-based tracking knows how much, when it renewed, and when it was refunded. For subscriptions only the second is correct.
Invoice-level attribution means a partner commission is calculated from the Stripe invoice that was actually paid, not from a pixel that fired on a thank-you page. For a one-time purchase the two methods agree. For a subscription they disagree every month: the pixel fires once and then goes quiet, while the invoices keep arriving, changing amount, failing, retrying, and sometimes being refunded. If your program pays recurring commission, the only way to get the number right is to read the invoice.
This post explains how we attribute at the invoice in RelayWonder, why the ledger that stores the result is append-only, and what happens in the awkward cases: upgrades, downgrades, failed payments, refunds, disputes and customers who clicked two partners. We include the arithmetic so you can check our reasoning against your own program.
What a thank-you-page pixel actually knows
The classic affiliate script does three things. It reads a click id from the landing URL, it stores that id in a cookie, and on the confirmation page it sends a conversion event with whatever amount the page template happens to expose. That amount is usually the first charge. Sometimes it is the plan price before tax and before a coupon. Sometimes, when the checkout is a free trial, it is zero.
Here is what the pixel cannot know, because the page only loads once:
- Whether the trial converted into a paid subscription fourteen days later.
- Whether the customer renewed in month two, three and twelve.
- Whether they upgraded from $29 to $99 in month four.
- Whether the card failed in month six and the subscription was cancelled after the dunning emails.
- Whether a refund was issued in month two, or a chargeback arrived in month three.
Programs built on this kind of Stripe affiliate tracking end up with one of two bad outcomes. Either they pay a one-time commission on the first charge and quietly under-reward the partners who bring customers that stay, or they estimate recurring commission from the first charge multiplied by an assumed lifetime, which over-pays for churned customers and under-pays for upgrades. Both are approximations of a number that Stripe already holds exactly.
How invoice-level attribution works end to end
The flow has four steps, and only the first one runs in the browser.
- The tracking script reads the partner reference from the landing URL and stores it in a first-party cookie on the brand's own domain. The default window is 60 days. If a personal coupon code is used at checkout, the code itself carries the attribution and no cookie is needed.
- When the visitor starts checkout, the brand passes that reference to Stripe. With Stripe Checkout this is the client_reference_id field on the session; with a custom flow it is a metadata key on the customer or the subscription [1][2]. Either way the reference now lives on the Stripe object, not in the browser.
- RelayWonder listens to the brand's Stripe webhooks. Every invoice.paid event for a customer with a partner reference becomes one commission row: invoice id, paid amount, currency, date, partner, commission rate, commission amount [3].
- Every charge.refunded and charge.dispute.created event that relates to one of those invoices becomes a negative row against the same partner, with the same invoice id for traceability [4][5].
Nothing in this chain depends on a page loading. If the customer renews from an email link, from the Stripe-hosted customer portal, or automatically while asleep, the invoice is still paid, the webhook still fires, and the row still lands. That is the whole argument for invoice-level attribution in one sentence: the source of truth is the event that moved money, not the page that announced it.
A worked example over twelve months
Take a partner on 20% recurring commission with no time cap. A customer they referred signs up for a $49/month plan on 3 January, upgrades to $129/month on 5 May, requests a refund of one month in June after a billing complaint, and cancels at the end of October. The pixel saw a single $49 conversion. The invoices tell a different story.
| Month | Invoice event | Invoice amount | Commission row |
|---|---|---|---|
| Jan | invoice.paid (new subscription) | $49.00 | +$9.80 |
| Feb | invoice.paid (renewal) | $49.00 | +$9.80 |
| Mar | invoice.paid (renewal) | $49.00 | +$9.80 |
| Apr | invoice.paid (renewal) | $49.00 | +$9.80 |
| May | invoice.paid (upgrade, prorated) | $129.00 + $74.84 proration | +$40.77 |
| Jun | invoice.paid, then charge.refunded | $129.00, refund $129.00 | +$25.80 then −$25.80 |
| Jul | invoice.paid (renewal) | $129.00 | +$25.80 |
| Aug | invoice.payment_failed, retry succeeds | $129.00 | +$25.80 (on the paid event only) |
| Sep | invoice.paid (renewal) | $129.00 | +$25.80 |
| Oct | invoice.paid (final) | $129.00 | +$25.80 |
| Nov | customer.subscription.deleted | $0 | no row |
A few things to notice. The proration in May is not an estimate; Stripe issues an invoice line for the unused portion of the old plan and the remaining portion of the new one, and we commission the amount that was actually paid, $74.84 in this case, which is 129 minus 49 multiplied by the 29 of 31 days remaining in the cycle [6]. The refund in June does not edit the June commission row to zero. It adds a second row of −$25.80. The August payment failure produces no row at all, because invoice.payment_failed is not money moving; the row comes when the retry succeeds. The cancellation in November produces nothing either.
Summed, the partner earned $183.17 from this one customer. That is 18.7 times what the pixel would have paid. If a brand has forty such customers, the gap between pixel accounting and invoice accounting is the difference between a program partners talk about and one they forget.
Why the ledger is append-only
There is a tempting shortcut when a refund arrives: find the original commission row and set its amount to zero. We do not do that, and we think no program should. An append-only ledger is one where rows are inserted and never updated or deleted. Balance is a sum over rows. Clawbacks are negative rows. The consequences are practical rather than philosophical.
- A partner's March statement is the same number in April, in December and at tax time. If you edit March rows in June, the statement you emailed in April is now wrong and nobody knows.
- A payout batch can be reconciled. The batch exported on 1 April covers rows with a date before 1 March that had cleared the 30-day hold. If one of those rows is later refunded, the clawback shows up in the May batch as a negative line, which is exactly what the brand's accountant expects to see.
- Disputes can be audited. When a partner asks why their balance went down, the answer is a row with a timestamp, an invoice id and a Stripe event id, not a vague "adjustment".
- Fraud review has a trail. If a referral is flagged and the hold is extended, that is a status on the row, not a deletion of it.
The cost of this model is that balances are computed, not stored. For a program with tens of thousands of rows that is still a trivial query. We considered it a non-issue and have not changed our view.
Holds, minimums and when a row becomes payable
A commission row exists the moment the invoice is paid, but it is not payable until the hold period passes. The default hold is 30 days, which is chosen to be longer than the common refund windows and than most card issuers' early dispute activity. A brand can set it longer. Rows that have cleared the hold are added up per partner per currency, and when the total reaches the minimum payout, $50 by default, the partner appears in the next monthly batch.
The interaction with negative rows matters. A −$25.80 refund row is applied immediately; it does not wait 30 days. So a partner whose only cleared row is +$25.80 and whose latest row is −$25.80 has a payable balance of zero. If a negative row arrives after the positive row has already been paid out, the partner's balance goes negative and the next positive rows fill the hole before anything new becomes payable. We never ask a partner to send money back; the ledger nets it against future earnings.
Edge cases and how we resolve them
Two partners, one customer
A visitor clicks partner A's link in week one and partner B's link in week three, then subscribes. The default is last click within the window, so the cookie is overwritten and B is attributed. If the visitor instead uses A's personal coupon code at checkout, the code wins, because a code typed at checkout is a stronger signal than a cookie set weeks ago. The brand can see both events on the customer's timeline and override manually; the override is itself a row.
Free trials
A trial creates a subscription and, in most Stripe configurations, a $0 invoice. We record that invoice as a row with zero amount so the referral is visible, then the first real invoice at the end of the trial produces the first non-zero commission. The 60-day cookie window applies to the click-to-subscription step, not to the trial length; a 30-day trial that converts on day 31 is still attributed because the subscription was created on day one.
Annual plans and multi-currency
An annual plan produces one invoice per year, so the commission row is large and the hold matters more. We do not split annual commission into twelve monthly rows; one invoice, one row. Currency is taken from the invoice, and the partner holds one balance per currency, so a partner referring customers to a EUR-billing brand and a USD-billing brand sees two balances and receives two lines in the payout batch.
Failed payments and dunning
invoice.payment_failed produces no row. Stripe's smart retries may succeed days later and fire invoice.paid; that is when the row is created, dated the day it was paid. If the subscription is eventually cancelled for non-payment, nothing further happens. The partner was never credited for money that never arrived.
Disputes that are won
charge.dispute.created produces a negative row. If the brand later wins the dispute, charge.dispute.closed with a won status produces a positive row that restores the amount. Again, two rows, not an edit. The partner sees the dip and the recovery with dates on both.
Setting it up in an afternoon
The implementation effort for a brand already on Stripe Billing is small, and most of it is passing one field.
- Add the tracking snippet to the marketing site. It sets the cookie and nothing else.
- At checkout creation, read the cookie server-side and pass it as client_reference_id (Checkout) or as metadata (custom flow). If you use Stripe Payment Links, the reference can be appended as a URL parameter [7].
- Add the RelayWonder webhook endpoint in the Stripe dashboard and subscribe to invoice.paid, charge.refunded, charge.dispute.created, charge.dispute.closed and customer.subscription.deleted. Stripe signs every event; we verify the signature before reading it [3].
- Set the commission terms: rate, recurring or first-invoice only, optional duration cap, hold period, minimum payout.
- Run one test subscription in Stripe test mode and watch the rows appear.
We do not ask for restricted keys that can create charges or read bank details. The webhook carries everything attribution needs. If a brand would rather we read invoices by API instead of webhooks, a read-only restricted key with invoice and charge scope is sufficient.
How other tools do it
Invoice attribution is not unique to us, and we would be suspicious of any subscription affiliate tool that did not do it. Rewardful and FirstPromoter both attribute from Stripe events and both support recurring commission; FirstPromoter also integrates Paddle and Chargebee, and Rewardful supports Paddle [8][9]. Where tools differ is in whether the stored result is editable, whether refunds are automatic negative entries or manual adjustments, how holds interact with clawbacks, and whether the partner keeps one account across brands. Those are the questions to ask on a demo call, because the webhook subscription itself is table stakes. As checked on 2026-09-18, both pricing pages describe tiering by attributed revenue with no commission cut, which matches our own model, so the comparison is on ledger behaviour rather than on price.
We do not yet support Shopify. When we do, order webhooks will play the role invoices play here and refunds will be negative rows in the same way; the ledger model does not change, only the source. We mention it so nobody reads this post as a promise of something that is not shipped.
What to check in your own program
If you already run a subscription affiliate program, three quick checks tell you whether you have invoice-level attribution or click-level attribution.
- Pick a referred customer who upgraded. Does the partner's commission change in the month of the upgrade? If not, you are paying on the first charge.
- Pick a refund from last quarter. Can you find a negative line with that invoice id in a partner statement? If the statement was simply recalculated, your history is not stable.
- Export last month's payout batch. Does every line trace to a list of invoice ids? If a partner asks, could you show them?
Getting these right is less about software features and more about deciding, once, that the invoice is the unit of truth. That decision is what invoice-level attribution means in practice, and everything else in the ledger follows from it.
FAQ
What is invoice-level attribution in an affiliate program?
It means each commission is computed from a paid invoice in the billing system rather than from a conversion event on a web page. Every renewal, upgrade and refund produces its own entry, so recurring commission reflects what the customer actually paid over time.
Does the customer need to keep the cookie for renewals to be attributed?
No. The partner reference is attached to the Stripe customer or subscription at the first checkout. After that, every invoice for that subscription is attributed from the Stripe object, and the cookie is irrelevant.
What happens to commission when a customer gets a refund?
A negative row equal to the commission on the refunded amount is added against the partner, referencing the same invoice. The original row is never edited. If the partner has already been paid, the negative balance is netted against future earnings.
How are prorated upgrade invoices handled?
Stripe issues an invoice containing the proration lines, and the commission is calculated on the amount actually paid on that invoice. In our example a mid-month move from $49 to $129 produced a $74.84 proration and a commission of 20% of that, about $14.84, on top of the regular renewal.
Why not pay commission on invoice.payment_failed when a retry is likely?
Because no money has moved. If the retry succeeds, invoice.paid fires and the row is created on that date. If it never succeeds, no row was ever created and there is nothing to claw back.
Do you support Paddle, Chargebee or Shopify?
Today attribution is built on Stripe webhooks. Shopify is on the list but not available; when it ships, order webhooks will feed the same append-only ledger. We would rather say that plainly than list integrations we have not built.
Sources
- Stripe Docs: Checkout Session client_reference_id · Reference field used to carry the partner identifier onto the session and resulting customer.
- Stripe Docs: Metadata · Key-value storage on customers and subscriptions for custom checkout flows.
- Stripe Docs: Webhooks · Event delivery, signature verification and the invoice.paid event family.
- Stripe Docs: Refunds · charge.refunded events and partial refunds.
- Stripe Docs: Disputes · charge.dispute.created and charge.dispute.closed lifecycle.
- Stripe Docs: Prorations · How mid-cycle plan changes produce proration invoice lines.
- Stripe Docs: Payment Links · Passing client_reference_id as a URL parameter on hosted links.
- Rewardful pricing · Plans tiered by attributed revenue, Stripe and Paddle; checked 2026-09-18.
- FirstPromoter pricing · Plans tiered by affiliate-driven revenue, Stripe, Paddle and Chargebee; checked 2026-09-18.