Revenue attribution breaks when the person who paid is not the same browser session you tagged Tuesday. Metadata is how you glue first touch to Stripe customer without cookies across devices.
Part of the Stripe revenue guides. Read the main guide for the full picture.
Fields worth copying
Keep names consistent with analytics:
utm_sourceutm_mediumutm_campaignutm_content(optional)landing_path(first path in session)first_seen_at(ISO timestamp)
Stripe metadata keys are strings; keep values short. Avoid stuffing entire query strings into one key.
Checkout Session metadata vs Customer metadata
| Location | Best for |
|---|---|
| Checkout Session | One-time purchases, clear session boundary |
| Customer | Subscriptions, returning upgrades |
Pattern for subs: write UTMs to Customer on first successful checkout; update only if empty (first touch wins). Overwriting on renewal checkout erases history.
Passing UTMs from the browser
Common flows:
- Hidden fields on pricing form populated from sessionStorage before redirect to Stripe Checkout.
- Server creates Session: API route reads cookies/session server-side (if you have login) and sets
metadataoncheckout.sessions.create. - Payment Link: harder; prefer programmatic Session create for tagged campaigns.
Test: complete checkout from a tagged URL, open Stripe Dashboard → Customer → metadata visible.
Client_reference_id trick
Some teams store session_id from analytics in client_reference_id, then join in warehouse. Indie stack: store both session_id and UTMs in metadata for simpler debugging when join fails.
Webhook handler: read metadata once
On checkout.session.completed:
- Parse metadata from session or customer object.
- Emit revenue event with same fields analytics already uses for pageviews.
- Dedupe by
payment_intentorsession.id.
Missing metadata should flag attribution=unknown, not silently attribute to direct.
Logged-in users
If signup happens before pay, persist UTMs on the user row at registration from first session, copy to Stripe when they upgrade. Registration without UTMs capture loses channel forever.
GDPR note
UTMs in Stripe are not highly sensitive; still avoid email in metadata keys. Customer id is enough.
Failure modes
- Metadata never set on Payment Links used in email, email looks like
(direct)revenue. - Zap overwrites customer metadata on every invoice.
- Test mode metadata copied to production customer during messy key rotation.
QA script before launch
- Open incognito, visit
/?utm_source=qa-test&utm_medium=manual. - Start checkout, complete with test card in test mode.
- Confirm webhook payload contains
utm_source=qa-test. - Confirm analytics revenue row matches.
Repeat on mobile, mobile Safari drops sessionStorage on cross-site redirects if misconfigured.
When metadata is overkill
If 100% of revenue is direct from word-of-mouth and you never tag links, metadata will not invent channels. Fix UTM discipline first.
Cross-links
Metadata is five minutes in checkout.sessions.create and months of cleaner revenue tables. Copy first-touch once; let webhooks carry it forever.
Stripe CLI local testing
Run stripe listen --forward-to localhost:3000/api/webhooks/stripe while testing metadata. Log raw payload to a file once per quarter so you remember field names when Stripe API version bumps.
Metadata size limits
Stripe caps metadata per object. If you cram entire JSON, keys truncate. Store only attribution primitives, not full referrer URLs, hash long referrers if needed.
Payment Element vs Checkout
Payment Element embedded on your page keeps sessionStorage alive longer than redirect Checkout, metadata wiring differs. Document which flow you use in runbook so a contractor does not wire the wrong path.
Connect and marketplace accounts
If you are a platform, attribution may belong to connected accounts, out of scope for typical indie SaaS; if you use Connect for a specific reason, namespace metadata keys buyer_utm_source vs seller fields.
Replaying webhooks
Stripe Dashboard replay can duplicate revenue if dedupe keys missing. Idempotency on event.id is mandatory before marketing trusts totals.
Aligning with UTM dictionary
Metadata values should match allowed source and medium strings, otherwise analytics groups twitter and x separately and you argue with yourself.
Affiliate override
If an affiliate parameter ref exists, decide precedence: affiliate wins over UTMs or vice versa. Document in one sentence; engineers will implement wrong otherwise.
Server-side Session create (recommended pattern)
Browser redirect Checkout is not the only path. A server route that reads first-party cookies or session ids, then calls Stripe with metadata and optional customer_email, survives ad blockers and tab closes better than hidden fields alone. The pricing page posts JSON { plan: 'pro' } to /api/checkout; your server attaches UTMs from the visitor cookie set on first pageview. Log the outgoing metadata in structured logs (redact email) for one week after each pricing deploy.
Subscription upgrades and empty metadata
When an existing subscriber hits “upgrade to annual” in Portal, Stripe may not run through your marketing Checkout at all. Policy: never overwrite Customer metadata on upgrade events; only fill empty keys on customer.created or first paid checkout. Expansion revenue rows can use medium=product while acquisition rows keep original utm_campaign.
Stripe Tax and metadata
Tax calculation changes line items, not attribution fields. Do not store tax amounts in utm_campaign jokes aside, keep metadata for marketing dimensions only. Multi-currency display can confuse humans reading Dashboard; analytics should still key off currency on the webhook payload.
Linking to path funnels
Metadata landing_path is the bridge to path funnels: revenue by /blog/guide vs /pricing even when checkout was hosted. If you only store utm_source, you know the channel but not which page started the story. Store both when session storage is available at checkout click.
Payment Link query parameters (limited)
Some teams append UTMs to the Payment Link URL itself for analytics on the click, then still need metadata on the underlying price for webhook attribution. Clicks without metadata on the Stripe object remain fragile. Prefer programmatic Session create for anything you will read in a launch postmortem.
Team handoff template
Checkout metadata keys (production):
utm_source, utm_medium, utm_campaign, utm_content (optional)
landing_path, first_seen_at, session_id (optional)
First-touch rule: set Customer metadata only if key empty
Webhook events: checkout.session.completed (acquisition), invoice.paid (renewals excluded)
Test URL: /?utm_source=qa-test&utm_medium=manualPaste into your repo README next to the webhook route.
Common engineering mistakes
- Reading metadata from the wrong object (Session vs Customer) on subscription renewals.
- URL-decoding UTMs twice, turning
email+newsletterinto garbage. - Stripping
utm_contentbecause “optional”, then thread tests are blind forever (thread UTMs).
When joins fail anyway
Metadata can be perfect and you still lose cross-device buyers. Report attributed % of revenue monthly; 70% attributed on self-serve SaaS is often healthy. Chasing 100% with fingerprinting is not worth it on indie stacks. Invest instead in newsletter UTMs that survive redirects on buy links.
Checkout Session subscription_data.metadata
For subscription checkouts, some teams only set Session metadata and forget subscription_data.metadata, renewals then lack campaign context on Subscription object in Dashboard. Stripe docs evolve; verify your API version once per year and copy working payload to internal runbook.
Local development and metadata
.env.local Stripe keys with test metadata utm_source=dev leak into shared staging databases if you sync data carelessly. Filter utm_source=dev and qa-test in production dashboards permanently.
Copy-paste metadata for contractors
Ask implementers to paste the live checkout.sessions.create metadata block into the PR description. Reviewers verify keys without reading three hundred lines of checkout UI code.