Stripe Meta Conversions API: Server-Side Purchase Events
By Vlad Yasinski·
A Stripe Meta Conversions API setup sends each confirmed payment from your server to Meta, tagged with the same event_id as the browser Pixel event, so Meta counts one purchase. You build it in three parts: store identifiers and an event_id before checkout, listen to a Stripe webhook, and post a Purchase event from that webhook. Done right, the purchase count in Ads Manager follows real payments.
Browser pixels miss a share of purchases. Sending the Stripe payment from your server to Meta, with a shared event_id, fixes the count without double reporting.
Key takeaways
- A Stripe-to-Meta Conversions API setup sends each confirmed payment from your server, so purchase counts follow real money instead of what the browser reported.
- Meta merges a browser Pixel event and a server event into one purchase when event_name and event_id match on both sides and the events arrive within Meta's 48-hour window.
- Generate the event_id once at checkout start, store it with the Stripe session, and reuse it in the webhook.
- Meta does not remove refunded orders, so reconcile refunds in your own BI and judge ROAS on net payments.
Why the browser pixel alone undercounts
The Pixel runs in the visitor's browser, so anything that blocks or interrupts the browser also blocks the event. Ad blockers, browser privacy limits on cookies and scripts, and in-app browsers can stop the Pixel from loading or from storing identifiers. A redirect between Stripe checkout and your thank-you page adds another gap: the buyer pays, closes the tab, and the Purchase event never fires.
Meta optimizes delivery on the events it receives. If a share of real purchases never arrives, the system learns from a thinner and skewed sample, and ROAS in Ads Manager reads lower than your payments say. The size of the gap depends on your audience, device mix and checkout design, so measure it on your own funnel instead of assuming a number.
A server event sent after Stripe confirms payment is the source of truth for revenue. Stripe knows whether money moved. The browser knows that a page loaded. Keep the Pixel for upper-funnel events and for matching, and let the server own Purchase.
The event flow, step by step
The flow has two paths that end in one event inside Meta. In the browser, the Pixel fires PageView on landing, InitiateCheckout when the buyer starts checkout, and Purchase on the success page, with an event_id attached to it.
On the server, Stripe sends a webhook when payment completes. For Checkout Sessions that is usually checkout.session.completed, and for custom PaymentIntent flows it is payment_intent.succeeded. Your endpoint verifies the Stripe signature, confirms the payment status is paid, since some payment methods complete a session before money arrives, reads the data you stored for that session, and calls the Conversions API with a Purchase event. Delayed payment methods arrive as unpaid and finish later through checkout.session.async_payment_succeeded, so handle both events. For a subscription with a free trial, the first charge comes later, so send StartTrial from checkout.session.completed and send Purchase from the first invoice.paid event with an amount above zero.
Both calls carry the same event_name and the same event_id, and Meta merges them into one event. If the browser event is missing, the server event still counts. If both arrive, you get one purchase instead of two.
What to store before checkout
The webhook runs on your server after the buyer has left the page, so it knows only what you saved earlier. Capture these values before the buyer goes to Stripe:
- Fbp and fbc: read the _fbp and _fbc cookies. If fbc is missing but the URL carries an fbclid parameter, build fbc from it using the format in Meta's documentation.
- Page context: the landing URL for event_source_url, plus the buyer's IP address and user agent at checkout start.
- Event_id: one unique value generated when checkout starts.
- Contact data: email and, if you collect it, phone.
Stripe session metadata is a convenient place for these values, and your own database keyed by session id works as well. Metadata has size limits, so check Stripe's documentation and keep long values on your side. Create the event_id once and pass the same value to the browser Purchase and to the webhook.
Normalize email and phone before hashing: trim whitespace, lowercase the email, strip formatting from the phone number and include the country code. Then hash with SHA-256. Meta's documentation lists which fields are sent hashed and which, such as fbp, fbc, IP and user agent, are sent as they are, so confirm against the current version.
Building the server payload
A Purchase event needs event_name, event_time as a Unix timestamp, action_source set to website, event_source_url and user_data. For Purchase, add custom_data with value and currency. Required and recommended fields change over time, so check Meta's Conversions API documentation before you ship.
Decide up front whether you report gross or net value, and keep that choice consistent. Send the amount charged, taken from Stripe. Stripe reports amounts in the smallest currency unit, so divide by 100 for USD, while zero-decimal currencies such as JPY need no division. Skip deductions for fees or taxes you cannot verify per order.
Illustrative example: a buyer pays a $9.99 first charge, and Stripe reports amount 999 in cents. You divide 999 by 100 and send value 9.99 with currency USD, the event_id saved at checkout start, and the event_time of the payment confirmation. A renewal 30 days later is a separate payment with its own event_id. Meta deduplicates only events that arrive within 48 hours of the first event with that id, so the id exists to pair a browser event with its server twin.
Deduplication rules that hold up
Meta deduplicates when event_name and event_id match between a browser event and a server event, within a 48-hour window that Meta documents and may change, so confirm the current figure. Meta also documents a second method that matches event_name with fbp or external_id, which it describes as working for events sent first from the browser and then through the server. Meta recommends event_id. Both sides must agree on both values, including capitalization, because Purchase and purchase are different names.
The common failure is generating event_id separately in the browser and on the server. Each side sends a valid event with a different id, and Meta counts both. Take an illustrative day with 200 real first payments at $9.99, where the Pixel catches 150 and the server sends all 200. Without matching ids, Meta shows 150 + 200 = 350 purchases, worth 350 × 9.99 = $3,496.50, against real revenue of 200 × 9.99 = $1,998. With matching ids, the 150 browser events merge with 150 server events and 50 server-only events remain, so Meta shows 150 + 50 = 200 purchases.
Test in Events Manager with the Test Events tool, using a real test purchase. Check that the event arrives from both Browser and Server and shows as deduplicated. Then compare the day's Purchase count in Events Manager with successful payments in Stripe for the same time zone.
Trials, renewals and refunds
Choose which event represents value. A trial start carries no money, so you can send a separate event for it, such as StartTrial or a custom event, to optimize on early, and send Purchase once money moves. Trial events give Meta more signals, but trials that never convert to payment count too, so compare against paid conversions in your own data.
Send renewals to Meta only if you want delivery to optimize on them. Many teams keep optimization on the first payment and track renewals in BI for lifetime value. If you do send them, give each its own event_id and consider a distinct event name so they stay apart from first purchases in reporting.
Refunds are not removed from Meta, and a refunded order still counts as a purchase there. Join Stripe refunds to orders and traffic sources in your own BI, and judge ROAS on net payments.
Privacy and consent
Under GDPR or similar rules, send events only for users whose consent covers advertising use. The consent state has to reach your server, so store the consent flag with the session next to the other identifiers and skip the Conversions API call when consent is absent.
Hash identifiers as Meta requires and keep raw personal data out of custom parameters. A plain email in a custom field defeats the hashing. Limit the payload to what matching and value reporting need.
Document what you send, to whom, and on what basis. Valid consent and the fields you may share are configuration choices for you and your legal counsel, and no default setting decides them.
A 10-point verification checklist
I built web funnels end to end with Stripe checkout, Meta Pixel and Conversions API, and read performance on actual payments, so this checklist ends in a comparison against Stripe. Start with the setup checks:
- The Purchase event name is identical in browser and server code.
- The event_id is created at checkout start and stored with the session.
- The webhook verifies the Stripe signature, treats unpaid sessions as pending and handles checkout.session.async_payment_succeeded.
- Duplicate webhook deliveries are handled by storing processed Stripe event ids, so a retry does not send a second purchase.
- Fbp, fbc, IP, user agent and hashed email reach the server payload.
Then run the monitoring checks over a 7-day window:
- Deduplication status in Events Manager shows browser and server events merging.
- Match quality for Purchase is reviewed, and missing parameters are fixed at the source.
- The 7-day Purchase count in Events Manager is compared with successful Stripe payments in the same time zone.
- Five sampled orders match Stripe on time zone, currency and value.
- An alert fires when daily server events drop to zero.
Need server-side purchase events and a revenue-based dashboard on your funnel? Start with a tracking review.
See Web2App and Web2Web funnels