Sending Stripe purchases to Meta CAPI without redirect-page attribution loss
Updated July 14, 2026 · 11 min read
Stripe Checkout breaks many Meta implementations because the browser success page and the Stripe webhook observe different worlds. The success page has cookies but is not authoritative for money. The webhook is authoritative for money but has no browser identity unless you carried it there.
A durable implementation treats checkout creation as the handoff point: copy the attribution envelope into Stripe metadata, then emit Purchase from the verified webhook with the same event_id used by any browser-side Purchase.
Do not use the success page as the source of truth
- The success page can be blocked, abandoned, refreshed, opened twice, or loaded before asynchronous payment methods settle.
- The success page may still have _fbp and _fbc, but it does not prove that a paid invoice or completed payment exists.
- The webhook proves payment state, but only sees Stripe IDs unless checkout metadata carries visitor identity and event_id.
Checkout creation should freeze attribution metadata
The checkout session is where browser identity and billing identity meet. If this metadata is missing, the later webhook cannot reconstruct the original visitor.
const eventId = `purchase:${cartId}:${crypto.randomUUID()}`;
await stripe.checkout.sessions.create({
mode: "subscription",
line_items,
success_url,
cancel_url,
client_reference_id: visitorId,
metadata: {
meta_event_id: eventId,
visitor_id: visitorId,
landing_session_id: landingSessionId,
fbp: identity.fbp ?? "",
fbc: identity.fbc ?? "",
landing_url: identity.landingUrl,
},
});Webhook emission protocol
- 1Listen to checkout.session.completed for immediate card payments, but confirm the payment or subscription state before sending revenue.
- 2For subscriptions, prefer invoice.paid as the recurring revenue source and keep first purchase vs renewal semantics explicit.
- 3Use Stripe object IDs and stored meta_event_id to make CAPI emission idempotent across webhook retries.
- 4Send value as the amount actually paid, after discounts and trial pricing, in major currency units with ISO currency.
- 5Send user_data from checkout metadata plus any authenticated user fields available on your own customer record.
CAPI Purchase payload shape
const capiEvent = {
event_name: "Purchase",
event_time: Math.floor(Date.now() / 1000),
event_id: session.metadata.meta_event_id,
action_source: "website",
event_source_url: session.metadata.landing_url,
user_data: {
external_id: [sha256(session.metadata.visitor_id)],
fbp: session.metadata.fbp || undefined,
fbc: session.metadata.fbc || undefined,
em: customer.email ? sha256(customer.email) : undefined,
client_ip_address: identity.ipAddress,
client_user_agent: identity.userAgent,
},
custom_data: {
value: amountPaid / 100,
currency: currency.toUpperCase(),
order_id: session.id,
},
};Frequently asked questions
Should Stripe Purchase be sent from the browser or webhook?
The authoritative Purchase should be sent from the webhook after payment is verified. A browser-side Purchase can also be fired for identity, but it must share the same event_id so Meta deduplicates it.
Where should I store _fbp and _fbc for Stripe Checkout?
Capture them before creating the checkout session and store them in your own order row or Stripe metadata. The Stripe webhook request itself will not contain the user's browser cookies.
How do I avoid duplicate purchases from Stripe webhook retries?
Use idempotency at your own CAPI emission layer keyed by Stripe event ID, Stripe object ID, and the Meta event_id. Webhook delivery can retry; Meta conversion emission should remain single-intent.
Related guides
Not sure which signal path is breaking?
Zoruko audits your web and mobile code for broken Meta and TikTok signal chains, then ships each fix as a pull request you review.
Run a free audit