How to set up Stripe metadata attribution
Write a visitor ID and UTM fields into Stripe metadata for Checkout, Payment Links and subscriptions, and see which copies Stripe makes and which it skips.
Muzahid Maruf, FounderUpdated
On this page
Explore with AI
Opens this article inside the chosen assistant with a ready-made prompt.
Stripe metadata attribution setup means writing a visitor ID and 6 UTM fields into the metadata of the Stripe objects that carry a payment, so one report can show which channel pays your MRR.
Stripe forwards metadata in only 4 documented cases, so a source set on the Checkout Session alone reaches none of the invoices and charges that follow.
Key takeaways
- Stripe allows 50 metadata pairs per object, with keys up to 40 characters and values up to 500, so a visitor ID plus 6 UTM fields uses 7 slots.
- A Checkout Session's metadata is not copied to its PaymentIntent or Subscription. Set payment_intent_data.metadata or subscription_data.metadata yourself.
- Invoices created from June 29, 2023 hold a snapshot of the subscription's metadata, while the renewal charge holds none, so read renewals from the invoice.
- client_reference_id holds up to 200 characters and is the per-click field for Payment Links, where UTM codes reach only your redirect URL.
- Stripe Search finds metadata on 7 object types within 1 minute, but not on Checkout Sessions.
What Stripe metadata can hold
Stripe keeps metadata on Customers, Checkout Sessions, PaymentIntents, Charges, Subscriptions, Invoices and other objects.
I'd start with a single key, vid, pointing at visit history you control, and copy UTM fields onto Stripe only to see the source in webhook payloads. Stripe's metadata guide recommends the same split for bigger records.
| Field | Limit | Consequence |
|---|---|---|
| Metadata pairs per object | 50 | A visitor ID plus 6 UTM fields uses 7 |
| Metadata key | 40 characters | utm_campaign takes 12 |
| Metadata value | 500 characters | Parse landing URLs into discrete values |
| Readable with | Secret keys only | Not from Stripe.js in the browser |
| client_reference_id | 200 characters | Fits a visitor ID, while campaign data goes in metadata |
| UTM codes on a Payment Link | 5 parameters, 150 characters each | Letters, digits, dashes and _ only |
| Checkout Session lifetime | 30 minutes to 24 hours, default 24 | Keys written at creation can be a day old at payment |
Limits from Stripe's metadata, Checkout Session and Payment Links documentation, October 2026.
Which Stripe objects inherit metadata
Metadata doesn't copy to related objects unless one of 4 documented exceptions applies, and Customer metadata never copies down, so look it up by customer ID.
Three exceptions are one-time snapshots, which leave the copy unchanged when you edit the parent later. The 4th shows the subscription's current values.
| Exception | From | To | What Stripe copies |
|---|---|---|---|
| 1 | PaymentIntent | Charge | A snapshot when the charge is created |
| 2 | Payment Link | Checkout Session | A snapshot when the Session is created |
| 3 | Subscription | Invoice | A snapshot in the invoice's parent details |
| 4 | Subscription | Invoice line item | The current values, for line items of type subscription |
Copy rules from Stripe's metadata documentation, October 2026.
Worked example: a $49 monthly plan
A newsletter reader buys a $49 plan through Stripe Checkout and stays 12 months, which produces 12 invoices and 12 charges worth $588. Metadata on the Checkout Session alone reaches 0 of those 24 objects. Adding subscription_data.metadata puts the source on the Subscription and all 12 invoices, and the 12 charges still carry none.
Per Stripe's Invoice reference, parent.subscription_details.metadata is immutable once the invoice is finalized and exists only on invoices created from June 29, 2023. Fix a mistyped utm_campaign after the 3rd invoice and those 3 invoices keep the typo.
A Charge in the current Stripe API has no invoice field, so report from invoices or the invoice.paid event, or go from a charge through its PaymentIntent to the Invoice Payments API.
Stripe metadata attribution setup, step by step
Use a Stripe sandbox. The test card 4242 4242 4242 4242, with any future expiry date and any 3-digit CVC, completes a payment, per Stripe's testing guide.
Step 1: Capture the source on your own domain
Read the 5 UTM parameters on the first page view and store them under a visitor ID in a first-party cookie or your database. Stripe never sees the ad that sent the visitor, because your server creates the Checkout Session.
Per WebKit's tracking prevention notes, Safari's Intelligent Tracking Prevention deletes cookies created in JavaScript after 7 days without user interaction, so set the cookie from your server where you can. Why UTM parameters get stripped covers the other leaks.
Step 2: Pick the keys once
Pick short, stable names, since webhooks and backfills depend on them. This schema uses 9 of the 50 slots.
| Key | Example value | Purpose |
|---|---|---|
| vid | A 36-character UUID | Join key to your stored visit history |
| ft_source, ft_medium, ft_campaign | newsletter, email, launch-2026 | First touch, written once and never overwritten |
| lt_source, lt_medium, lt_campaign | google, cpc, brand-2026 | Latest touch before checkout |
| click_id | a gclid or fbclid | Platform click ID for sending conversions back |
| first_touch_at | 2026-10-02T14:08:31Z | Time of the first visit in ISO 8601 |
Store both touches: 2 sets of 3 keys cost 6 slots and leave the credit rule, first-touch, last-touch or linear, for report time, as the attribution model comparison lays out.
Step 3: Write the keys onto the Checkout Session
In subscription mode, write 3 fields. The Session's own metadata (1 key, vid) arrives with checkout.session.completed. subscription_data.metadata (7 keys) lands on the Subscription and, through the snapshot, on every invoice.
client_reference_id takes up to 200 characters and comes back on the Session. In payment mode use payment_intent_data.metadata, which reaches the Charge as a snapshot, per the Checkout Session reference. The Stripe Checkout attribution guide covers the redirect side.
// visitorId and attr come from your own first-party capture (step 1)const session = await stripe.checkout.sessions.create({ mode: "subscription", line_items: [{ price: "price_123", quantity: 1 }], client_reference_id: visitorId, // up to 200 characters, echoed on the Session metadata: { vid: visitorId }, // lands on the Checkout Session subscription_data: { metadata: { // lands on the Subscription, then on each invoice vid: visitorId, ft_source: attr.firstSource, ft_medium: attr.firstMedium, ft_campaign: attr.firstCampaign, lt_source: attr.lastSource, lt_medium: attr.lastMedium, lt_campaign: attr.lastCampaign, }, }, success_url: "https://example.com/welcome",});// mode: "payment" takes payment_intent_data: { metadata: { ... } } instead.Step 4: Carry the ID through Payment Links
Stripe's tracking page says the 5 UTM codes on a Payment Link are passed to your redirect URL after payment, and only when the link's confirmation behavior is a redirect. Stripe doesn't describe saving them on the payment.
Two routes do reach it.
Route 1 is a Payment Link per channel, created through the API with metadata on the link, which copies to every Checkout Session, plus the nested metadata fields for the PaymentIntent or Subscription underneath. It suits 3 to 5 channels.
Route 2 is a per-click ID: append ?client_reference_id=VISITOR_ID to the link, using letters, digits, dashes and _ only, 200 characters at most. Stripe silently drops anything else, such as an ID with a dot or colon.
The Payment Link reference and the Payment Links attribution guide cover the rest.
curl https://api.stripe.com/v1/payment_links \ -u "<<YOUR_SECRET_KEY>>:" \ -d "line_items[0][price]=price_123" \ -d "line_items[0][quantity]=1" \ -d "metadata[lt_source]=newsletter" \ -d "subscription_data[metadata][lt_source]=newsletter"Step 5: Read the metadata back
Every Stripe event carries its object with that object's metadata, and most attribution work needs only these 4 events.
| Event | Object in the payload | Where the source sits |
|---|---|---|
| checkout.session.completed | Checkout Session | metadata and client_reference_id |
| customer.subscription.created | Subscription | metadata set through subscription_data |
| invoice.paid | Invoice | parent.subscription_details.metadata, the snapshot |
| charge.succeeded | Charge | metadata, payment mode only, copied from the PaymentIntent |
Event names from Stripe's event reference, October 2026.
Free trials have no charge yet, so trial-to-paid rates by channel start at customer.subscription.created. Count delayed payment methods on checkout.session.async_payment_succeeded. Verify the Stripe-Signature header and key rows by subscription ID, since deliveries repeat and arrive out of order.
| Behavior | Limit |
|---|---|
| Retries of a failed webhook delivery, live mode | 3 days, exponential backoff |
| Retries of a failed sandbox delivery | 3 attempts over a few hours |
| Resending an event from the Stripe Dashboard | 15 days |
| Resending an event from the Stripe CLI | 30 days |
| Metadata search coverage | 7 object types, no Checkout Sessions |
| Clauses in one search query | 10, joined by AND or OR but not both |
| Time until new data is searchable | Under 1 minute |
| Search rate limit | 20 reads a second |
| Oldest API version that supports Search | 2020-08-27 |
Delivery and search limits from Stripe's webhook and search documentation, October 2026.
Stripe Search takes metadata["vid"]:"VALUE" on charges, customers, invoices, PaymentIntents, prices, products and subscriptions, matching exactly and ignoring case. Stripe points analytics workloads to Stripe Sigma and bulk exports to Data Pipeline, and its webhook documentation has the signature details.
curl -G https://api.stripe.com/v1/subscriptions/search \
-u "<<YOUR_SECRET_KEY>>:" \
--data-urlencode 'query=metadata["lt_source"]:"newsletter" AND status:"active"'Grouping active subscriptions by lt_source gives MRR by channel.
| lt_source | Active subscriptions | Plan | MRR | Share of MRR |
|---|---|---|---|---|
| newsletter | 12 | $49 | $588 | 36.2% |
| 8 | $99 | $792 | 48.7% | |
| affiliate | 5 | $49 | $245 | 15.1% |
| Total | 25 | $1,625 | 100% |
Illustrative figures, not customer data.
Step 6: Test the chain before you trust it
Run a subscription checkout in the sandbox, then retrieve the Checkout Session, the Subscription, the first invoice and the charge with stripe get from the Stripe CLI. The Session shows 1 key and the Subscription shows 7.
The invoice shows the same 7 in its snapshot, and the charge shows none. Repeat with a payment-mode Session, where the charge should carry the PaymentIntent's keys.
Then open a Payment Link with ?client_reference_id=test-123 and confirm the value on the completed Session.
What TrackRev does with the visitor ID
TrackRev is SaaS affiliate software with revenue attribution built in. It reads Stripe with a read-only restricted key, so it never writes metadata.
Its pixel keeps a visitor ID in a first-party cookie named vid and adds it to Payment Links, Buy Buttons and Pricing Tables on your pages.
For Checkout Sessions your server creates, pass the cookie value as the Session's reference ID or set metadata.vid on the PaymentIntent.
The Stripe sync finds the Checkout Session through the charge's PaymentIntent, which Stripe documents for payment-mode Sessions only, so Subscription checkouts need identify().
| Order | Where each sync looks for the visitor ID |
|---|---|
| 1 | metadata.vid on the charge |
| 2 | client_reference_id on the Checkout Session |
| 3 | metadata.vid on the Checkout Session |
| 4 | The buyer's email, if you called identify() on your site |
Lookup order in TrackRev's Stripe sync.
Renewals inherit the first order's visitor through the subscription ID, so month 12 credits the original channel without any subscription metadata. The sync runs hourly.
Call identify() at signup too, since the pixel writes its cookie from JavaScript and Safari's 7-day cap applies. Revenue figures need a paid plan from Indie at $29 a month (pricing), and setup is on the Stripe integration page.
When metadata isn't enough
Metadata holds a snapshot of 1 or 2 touches taken at purchase, so keep the full history of visits and link clicks under the visitor ID, as in the multi-touch attribution setup guide.
If you only match each payment to a row in your orders table, a single reference ID is enough and 49 slots stay empty. Plan changes in the Stripe customer portal are covered in Stripe Customer Portal attribution.
Sales-led deals sent as Stripe Quotes can carry the source in the quote's subscription data, which lands on the Subscription on acceptance.
Found this useful? Share it.
Frequently asked questions
- No. Of the 4 cases where Stripe copies metadata, Checkout Session to PaymentIntent is not one. Set payment_intent_data.metadata in payment mode or subscription_data.metadata in subscription mode.
- Stripe copies subscription metadata onto the invoice, not the charge. Read the invoice's parent subscription details, or fetch the subscription. Invoices created before June 29, 2023 have no snapshot.
- Stripe passes the 5 UTM codes, up to 150 characters each, to your redirect URL after payment and doesn't store them on the charge. Use client_reference_id for a per-click value, or a link per channel with metadata set.
- Yes, for 7 object types, using metadata["key"]:"value". Checkout Sessions can't be searched. Matching is exact and ignores case, and fresh data usually appears within a minute.
- Use client_reference_id for the ID that rides on a Payment Link URL, and metadata for the Subscription, PaymentIntent or invoice. Writing the ID in both places costs 1 slot.
- No. TrackRev reads Stripe with a read-only key. It checks 4 places for the visitor ID, ending with the buyer's email through identify(). Revenue figures are on paid plans from Indie at $29 a month.

Written by
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
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