How to attribute Stripe Customer Portal upgrades and cancellations

Stripe Customer Portal attribution: why portal plan changes carry no UTMs, and how a stored source, webhooks and proration settings credit upgrades and churn.

Muzahid Maruf — Founder of TrackRev.io

Muzahid MarufUpdated

Revenue attribution · 8 min read
On this page
  1. 01Stripe Customer Portal attribution: what each action sends
  2. 02Why a portal change carries no marketing signal
  3. 03Store the source on the customer and the subscription
  4. 04Reading each event
  5. 05When a portal upgrade turns into cash
  6. 06Churn by channel from portal cancellations
  7. 07What TrackRev does with a portal change

Explore with AI

Opens this article inside the chosen assistant with a ready-made prompt.

Stripe Customer Portal attribution works only when the channel was saved before the customer first opened the portal.

Stripe hosts that page, so a plan change or cancellation reaches your systems as a webhook event with no UTM, cookie or referrer attached.

Take a customer who bought the $29 plan 11 months ago and upgrades to $79 in the Customer Portal: nothing in the event points to the newsletter issue that won them.

Save the source at signup against the Stripe customer and subscription IDs, then read it back when customer.subscription.updated arrives.

Key takeaways

  • Stripe hosts the Customer Portal and says it can't be shown in an iframe, so a plan change or cancellation reaches you only as webhook events with no UTM, cookie or referrer.
  • On a $29 plan upgraded to $79 on day 20 of 30, the net proration is $16.67: billed at once with always_invoice, on the $95.67 renewal with create_prorations, never with proration off.
  • A portal cancellation sends 2 events, customer.subscription.updated when the customer schedules it and customer.subscription.deleted when the period ends, and Stripe allows an undo in between.
  • TrackRev credits every later charge on a subscription to the channel of its first charge, so after a $29 to $79 upgrade every renewal still credits the original channel. It ignores subscription events.

Stripe Customer Portal attribution: what each action sends

The Customer Portal lets customers update payment methods, switch plans or quantities, cancel and read invoices. In the Stripe Dashboard, Switch plan, Update quantities and Prorate subscription updates start off, and Cancel subscription and Cancellation reason start on (configuration guide).

Portal actionEvents Stripe sendsMRR changeAnnualized
Switch a $29 plan to a $79 plancustomer.subscription.updated+$50+$600
Add 3 seats at $15customer.subscription.updated+$45+$540
Downgrade from $79 to $29, scheduled for period endsubscription_schedule.created-$50 at period end-$600
Cancel a $79 plan at period endcustomer.subscription.updated, then customer.subscription.deleted-$79 at period end-$948
Cancel a $79 plan immediatelycustomer.subscription.deleted-$79 now-$948
Undo a scheduled cancellationcustomer.subscription.updated$0$0
Replace a card or edit the billing addresspayment_method.attached, customer.updated$0$0

Event names from Stripe's portal, cancellation and subscription webhook documentation, October 2026. Prices are illustrative.

Customers can cancel but not update subscriptions with multiple products, usage-based billing, trial offers or invoice collection, and the plan picker lists at most 10 products.

Why a portal change carries no marketing signal

Stripe serves the Customer Portal from its own page and says it can't be displayed inside an iframe, so none of your scripts run there.

A new session URL expires after 5 minutes if unused, and a used one within 1 hour of the last activity. A custom domain can replace billing.stripe.com, but Stripe still serves the page.

The customer also arrives far from the click that won them.

The Google Analytics Measurement Protocol documentation says an event meant to join a gtag.js session should arrive within 48 hours, or it may not be processed as expected for conversion attribution.

An upgrade 11 months after signup lands about 7,920 hours after the visit, 165 times that window. Connecting Google Analytics to Stripe revenue covers the build that works for a first payment.

You still control 3 steps. Your server creates every session after authenticating the customer (integration guide), so that route can record the customer, the page the click came from and the session ID.

Stripe lists 5 flow types in its deep link guide, and a flow can return the customer to your own URL through after_completion, where your tag fires. Webhooks then report what changed.

Matching each webhook to the customer's latest logged session is a heuristic.

The no-code login link skips the session route: the customer enters an email and Stripe sends a login link.

When 2 or more customers share that email, Stripe picks the most recently created one with an active subscription, according to the no-code portal setup page.

Store the source on the customer and the subscription

The portal documentation says changes that take effect immediately go through the Subscriptions API and changes at period end go through Subscription Schedules, so the subscription ID and its metadata stay in place.

It serves subscriptions started in Stripe Checkout, Payment Links or the pricing table alike, so capture the source in whichever flow creates them, and write it to the Customer too.

A canceled subscription can't be reactivated (cancellation guide), so a returning customer needs a new subscription, and a source on the Customer carries over when your checkout reuses the customer ID.

Stripe metadata attribution setup has the key names and the 50-pair limit. I credit an upgrade to the channel that won the customer, since a plan change involves no new visit.

Reading each event

customer.subscription.updated also fires for renewals and coupons (subscription webhook guide), so compare previous_attributes with the new object.

Classify customer.subscription.updated (Node)
// After signature verification. Amounts are in cents.// Handles single-item subscriptions with per-unit prices, which is what the portal can change.function classify(event) {  const sub = event.data.object;  const prev = event.data.previous_attributes ?? {};   const monthly = (item) =>    (item.price.unit_amount * item.quantity) /    (item.price.recurring.interval === "year" ? 12 : 1);   const oldItem = prev.items?.data?.[0];  if (oldItem) {    const delta = monthly(sub.items.data[0]) - monthly(oldItem);    if (delta !== 0) return { type: delta > 0 ? "expansion" : "contraction", delta };  }  if (sub.cancel_at && prev.cancel_at === null) {    return { type: "cancel_scheduled", feedback: sub.cancellation_details?.feedback ?? null };  }  if (sub.cancel_at === null && prev.cancel_at) return { type: "reactivated" };  return null; // renewal, coupon or another field changed}

One cancel_at test covers both billing modes. Classic mode sets cancel_at_period_end to true and cancel_at to the period end for a portal cancellation, and flexible mode sets cancel_at alone (billing mode comparison).

When a portal upgrade turns into cash

The subscription event fires on the day of the change, but the money can arrive a cycle later. With prorations disabled, the customer pays the full new price when the next invoice is generated, per the proration guide.

The portal configuration takes none, create_prorations or always_invoice for features.subscription_update.proration_behavior. Take a $29 plan upgraded to $79 on day 20 of a 30-day cycle.

The credit for 10 unused days is $9.67 (29 x 10/30), the debit for 10 days at the new price is $26.33 (79 x 10/30), and the net is $16.67.

With always_invoice Stripe calculates that sum and generates an invoice for it at once.

proration_behaviorBilled on day 80Renewal invoice on day 90Billed after the change
none$0.00$79.00$79.00
create_prorations$0.00$95.67$95.67
always_invoice$16.67$79.00$95.67

Day 20 of a 30-day cycle that began on day 60, tax ignored. Stripe prorates to the second, so live invoices differ by a few cents.

Follow one newsletter subscriber through create_prorations.

DayWhat happensInvoiceMRR afterChannel total
0Signup on the $29 plan, source saved as newsletter$29.00$29$29.00
30Renewal$29.00$29$58.00
45Card replaced in the portalNone$29$58.00
60Renewal$29.00$29$87.00
80Upgrade to $79 in the portal, 10 days leftNone$79$87.00
90Renewal plus the $16.67 proration$95.67$79$182.67
120Renewal$79.00$79$261.67
135Customer schedules cancellation for day 150None$79$261.67
150Subscription endsNone$0$261.67

Illustrative customer, 30-day cycles, tax ignored.

A first-invoice report holds $29 of the $261.67, or 11%.

On day 80, previous_attributes holds the $29 item and the subscription now holds the $79 item, so the delta is 7,900 minus 2,900 = 5,000 cents, a $50 expansion dated day 80 while the cash arrives on day 90.

Record MRR from the subscription event and cash from invoice.paid. A downgrade applied now runs in reverse: negative prorations aren't automatically refunded, so no charge.refunded event fires and a later invoice is smaller.

Commission rules for these cases are in affiliate commissions on Stripe refunds and upgrades.

A switch to annual billing adds cash and removes MRR.

A $29 monthly plan moved to $290 a year lowers MRR from $29 to $24.17, a $4.83 contraction, and saves the customer $58 against 12 x $29 = $348, yet the next invoice jumps to $290 when the switch waits for period end.

The portal configuration reference counts that switch as a decreasing_item_amount, 1 of 2 conditions that can schedule a change for period end, and the portal schedules only between prices of the same product.

Churn by channel from portal cancellations

Date the churn decision from the first event, day 135 in the ledger, and the lost revenue from the second, day 150, or a channel's cancellations land up to a month late.

The cancellation page in Stripe's docs lists 3 places to read the customer's reason: the subscription page in the Stripe Dashboard, Stripe Sigma and the webhook payload.

Separately, Stripe can cancel a subscription automatically after failed payment under your billing settings, and cancellation_details.reason records that as payment_failed, or payment_disputed after a dispute (Subscription object reference).

FieldValuesHow to read it
cancellation_details.feedback8 options, from too_expensive to unusedThe customer's own reason, present only when they canceled explicitly
cancellation_details.reason4 values, including payment_failed and cancellation_requestedWhy Stripe ended the subscription. payment_failed is involuntary

Fields from Stripe's Subscription object and cancellation page documentation, October 2026.

Keep failed payments apart from chosen cancellations before you compare channels.

ChannelCustomersChose to cancelPayment failedChosen rateAll cancellations
Newsletter40357.5%20%
Paid social609315%20%

Illustrative numbers, 90 days after signup. Rates are cancellations divided by customers.

Lumped together, the 2 channels tie at 20%, while chosen cancellations run 7.5% against 15%. Read cohorts by age, since small cohorts mislead: 3 cancellations among 20 customers is 15% and one more makes it 20%.

For scale, TrackRev's Q2 2026 benchmarks report a median of 6.2 months to first churn for paid social customers against 12.4 months for organic search, across workspaces with at least 12 months of paid history.

Read a cluster of too_expensive answers in one channel as a price or targeting mismatch, and a cluster of missing_features as a gap between what the channel promised and what the product does.

What TrackRev does with a portal change

TrackRev is SaaS affiliate software with revenue attribution built in. It syncs Stripe charges every hour and, when the restricted key allows it, registers a webhook for 3 charge and Checkout events so new charges arrive sooner.

It ignores subscription events.

The first charge on a subscription is matched to a visitor through metadata.vid, the Checkout Session's reference ID or the buyer's email, and every later charge on that subscription ID copies the first order's channel and link split, rescaled to its own amount.

In the ledger, the $95.67 renewal and every $79 renewal after it credit the channel that won the $29 signup, provided that signup matched a click inside the window, with no tag on billing.stripe.com.

Because TrackRev ignores customer.subscription.updated, it doesn't label a change as expansion or contraction, doesn't record cancellations or their reasons, and has no churn-by-channel report in the product.

For those, run a handler like the one above or query your billing data directly.

Two limits apply. The first sync reaches back 30 days, so a subscription that began before you connected Stripe arrives without a source order.

Its first charge in TrackRev is matched against clicks inside your attribution window, which defaults to 30 days and accepts 1 to 365, so a customer who signed up 11 months ago gets no channel unless a tracked click fell inside it.

A customer who cancels and returns gets a new subscription ID, and the first charge is matched against recent clicks again instead of inheriting.

Revenue figures need a paid plan, starting at Indie for $29 a month. The free plan tracks links only, with 50 links and 1,000 tracked events a month (pricing).

Setup is on the Stripe integration page, and the renewal math is in subscription LTV attribution.

Found this useful? Share it.

PostLinkedIn

Frequently asked questions

Muzahid Maruf — Founder of TrackRev.io

Written by

Muzahid Maruf

Founder, TrackRev.io & Contant.io

Muzahid Maruf founded TrackRev.io, SaaS affiliate software with no limit on tracked revenue, and Contant.io. He writes about affiliate programs.

Writes about Marketing attribution · Link tracking · Revenue analytics · SaaS growth

  • Revenue attribution

    How to Measure Marketing ROI for SaaS

    Muzahid Maruf11 min
  • Revenue attribution

    Subscription LTV Attribution by Channel

    Muzahid Maruf10 min

Stop guessing where your revenue comes from.

Set up TrackRev in about five minutes. The free plan covers 1,000 events a month, no card needed.

Start free