Apple Pay completes in two seconds. Your tracker batches every three. The webhook hits your server before the browser flushes the pricing page view. Analytics shows revenue without traffic for that minute and you think the site is haunted.
Part of Stripe revenue attribution.
Why ordering inverts
- Redirect return from Checkout loads a success page that fires late.
- Webhooks are server-to-server, no ad blocker.
- User closes tab after pay before success page loads.
- Mobile backgrounding delays JavaScript.
None of these mean attribution is impossible; they mean session join cannot be the only key.
Primary fix: metadata before redirect
If UTMs and landing_path are on the Checkout Session before payment, webhook attribution does not depend on success pageview order. See Stripe metadata UTMs.
Secondary fix: success page as required step
Stripe success_url should load a page with synchronous track('checkout_completed') and visible confirmation. Do not rely on webhooks alone for client-side funnels.
Dedupe keys
Store stripe_session_id on both:
- Client conversion event (if fired).
- Webhook revenue row.
Merge duplicates in reporting; prefer webhook amount as source of truth for dollars.
Clock skew
Use server timestamps on webhook; align visitor events to UTC. Comparing “same minute” is fragile, attribute to day when joins fail.
Missing pageview, known revenue
Dashboard pattern:
- Revenue row with metadata UTMs → attributed.
- Revenue row without metadata, no session →
unknownbucket for manual review (check Stripe customer email domain, support chat).
Ad blockers on success page
If success page is on marketing domain and tracker blocked, webhook still wins with metadata. If metadata empty and tracker blocked, you get orphan revenue, fix metadata.
Load testing mistake
Synthetic load tests firing webhooks without pageviews create ghost revenue spikes. Tag test events environment=staging.
Support-assisted checkout
Founder sends Stripe Payment Link in Zoom, no site pageview. Attribute to medium=sales manually in metadata when creating link, or accept direct.
Monitoring alert
If daily webhook revenue count exceeds checkout pageviews by 2× for three days, instrumentation broke, not “viral growth.”
Cross-links
Race conditions are normal at indie scale. Metadata-first attribution is how you stop arguing with clock order.
Batch flush intervals
If tracker flushes every 3s, expect systematic webhook-first ordering on fast payments. Lower flush on pricing page only if performance allows, or rely on metadata.
Service worker caching
PWAs caching success page stale assets can fire duplicate client events, dedupe on session id + event type.
Invoice links emailed later
Customer pays invoice from email hours later, no fresh pageview. Metadata on invoice creation or customer record is the only fix.
Logging for one day
Temporarily log webhook arrival time and last pageview time per session_id when debugging, remove logs after fix to avoid PII hoarding.
Cross-device login then pay
User tags on phone, pays on desktop logged in, user row UTMs win if you stored at signup. Session join alone fails; this is expected.
Timeline diagram (mental model)
T+0s User clicks Pay
T+1s Stripe webhook → revenue row in DB
T+2s Success URL begins load
T+4s Tracker batch flush → pageview / success eventReporting that joins on exact timestamp fails at T+1s. Reporting that joins on session id + metadata succeeds if step 2 in checkout guide was implemented.
Funnel tools vs webhook truth
Client-side funnels count steps in the browser; webhooks count money on the server. When ordering inverts, funnel tools show “checkout completed” before “pricing viewed” for the same session, alarming until you realize the funnel is clock-sensitive. Prefer webhook dollars for revenue; use pageviews for path stories in path funnels.
Stripe Checkout on custom domain
Hosted checkout on checkout.stripe.com removes your tracker from the payment step entirely, expected. Pre-checkout metadata is non-optional. Post-checkout success URL on your domain should fire a synchronous conversion event before any celebratory confetti animation.
Edge: webhook retries
Stripe retries failed webhook delivery. Idempotent handlers prevent duplicate revenue; they also prevent duplicate “missing pageview” alerts. If your monitor compares counts, dedupe webhook event.id before alerting.
Attribution window fallback
When session join fails, attribute revenue to metadata UTMs with window unlimited for first-touch policy, or 7d for conservative last-touch, pick one and document. Mixing policies by mood makes channel tables untrustable.
QA matrix
| Scenario | Expect |
|---|---|
| Desktop Chrome, normal pay | Pageview then webhook or near-simultaneous |
| Apple Pay mobile | Webhook often first |
| Tab closed after pay | Webhook only; metadata required |
| Email invoice hours later | No pageview; metadata on invoice |
| Ad blocker on success page | Webhook only; metadata required |
Run the matrix in test mode before Black Friday, not after.
Connecting to RPV reviews
Orphan revenue rows (money without sessions) deflate denominator-less channel RPV if you assign them to unknown. Exclude unknown from channel RPV numerators and denominators separately in weekly slides, or founders think Twitter RPV dropped when plumbing broke.
Gumroad parallel
Digital product webhooks have the same ordering story with different hosts. Sale without same-day pageview covers email lag; Stripe covers speed lag. Same fix: tagged buy links and metadata, not fresher pageviews alone.
Real-time dashboards lie on launch hour
Live visitor counters update slower than webhooks during a coordinated email send. Founders screenshot “0 visitors, $400 revenue” and panic. Wait for batch flush or read webhook table directly; fix metadata if pattern persists past 24h.
Third-party analytics double firing
Google Analytics 4 and your first-party tracker may disagree on ordering. Pick one revenue source of truth (webhook) and one behavioral source (first-party pageviews). Do not reconcile GA4 purchase events to Stripe cent-for-cent on indie stacks.
Embedded checkout iframe
Iframe checkout can delay parent page events until postMessage handlers run, another webhook-first scenario. Parent page should listen for success message and fire conversion synchronously; still set Session metadata before iframe loads.
Support macro for orphan revenue
When support sees a paid customer with unknown attribution, macro reply is not the fix, metadata template on Payment Links is. Track count of unknown rows; if >5% of monthly new revenue, schedule engineering fix.
Pre-launch smoke test
One real card payment in live mode with tagged URL before any big send, confirms webhook, metadata, and success page ordering in production, not just Stripe test clock fantasies.
Marketing pulse pins during checkout regressions
If ordering breaks after a deploy, pin the broken week and label it checkout-tracking-incident so you do not compare channel RPV to clean weeks, marketing pulse guides notes are how future you avoids false “Twitter died” conclusions.
Success URL query params
Append session_id={CHECKOUT_SESSION_ID} to success URL when Stripe allows, gives client one more join key when metadata was accidentally omitted on one deploy branch. Treat it as belt-and-suspenders with metadata, not a substitute for first-touch UTMs.
Closing reminder
Webhook dollars are authoritative; pageviews are narrative. When order inverts, fix metadata and dedupe keys, do not chase millisecond joins in the database.