FloorSync / Guide for Stripe founders
Stripe to Meta Conversions API: every sale in Meta Ads
Meta’s ads learn from the purchases they’re told about. If your checkout is on Stripe, the pixel misses a share of them and every renewal. The fix is to send each sale from your server to the Conversions API. Here’s how, by hand or with FloorSync.
Short answer: in your Stripe webhook, on a paid checkout.session.completed, POST a Purchase to your pixel’s Conversions API endpoint with the hashed email, the fbp/fbc you kept at checkout, and the session id as event_id. Check it in Test Events.
1. What you need from Meta
- Your pixel (dataset) ID: Events Manager → your pixel → Settings.
- A Conversions API access token: same Settings page → Conversions API → Generate access token. Keep it on your server, never in the browser.
2. Which Stripe events become which Meta events
| Stripe event | Meta event | event_id |
|---|---|---|
checkout.session.completed, paid | Purchase, value = amount_total | the session id cs_… |
checkout.session.completed, $0 trial | StartTrial, value 0 | the session id |
invoice.paid, subscription_cycle | Your choice: Purchase (system_generated) or nothing. See step 04 of the check. | the invoice id in_… |
| refunds, failed payments, cancellations | Nothing: Meta can’t take a Purchase back. Take refunds off in your own numbers. |
3. Keep the buyer’s ids at checkout
The webhook arrives from Stripe’s servers, not from the buyer’s browser, so it carries no cookies. Read them when your server creates the Checkout session and keep them with it:
const session = await stripe.checkout.sessions.create({
// …your line_items, mode, customer_email…
success_url: 'https://yourbusiness.com/thanks?session_id={CHECKOUT_SESSION_ID}',
metadata: {
fbp: req.cookies._fbp || '',
fbc: req.cookies._fbc || '', // or build it from ?fbclid= on the landing page
ga: req.cookies._ga || '', // for GA4, see the GA4 guide
},
})
Stripe hands the same metadata back in checkout.session.completed. For renewals, keep them against the Stripe customer too, since the invoice won’t carry the session’s metadata.
4. Send the Purchase from your webhook
After your handler checks Stripe’s signature. Email is normalised (trimmed, lower-case) and SHA-256 hashed; Meta refuses it in plain text.
import { createHash } from 'node:crypto'
const sha = (s) => createHash('sha256').update(s).digest('hex')
const s = event.data.object // a paid checkout.session.completed
await fetch(`https://graph.facebook.com/v27.0/${PIXEL_ID}/events?access_token=${META_TOKEN}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
data: [{
event_name: 'Purchase',
event_time: event.created,
event_id: s.id, // same id as the pixel's Purchase on /thanks
action_source: 'website',
event_source_url: 'https://yourbusiness.com/pricing/',
user_data: {
em: [sha(s.customer_details.email.trim().toLowerCase())],
external_id: [sha(s.customer)],
fbp: s.metadata.fbp || undefined,
fbc: s.metadata.fbc || undefined,
},
custom_data: { value: s.amount_total / 100, currency: s.currency.toUpperCase() },
}],
// test_event_code: 'TEST12345', // while you check it in Test Events
}),
})
Meta takes events up to 7 days old. With action_source: 'website' it also wants the buyer’s IP and user agent for best matching; keep them at checkout with the cookies if you can.
5. Check it in Test Events
Events Manager → your pixel → Test events. Copy the code into test_event_code, send one event, and it shows there with the keys Meta matched on. If your thank-you page also fires the pixel, you should see the browser and server events paired, not counted twice. Then remove the test code.
Or: forward the event and let FloorSync do 3–5
FloorSync takes the verified Stripe event from your webhook and does the rest: hashes, ids kept from checkout (POST /api/v1/attribution), renewals matched to the first buyer, one send per event id, retries, and a log of every answer Meta gave. Python, from the docs:
import hashlib, os, requests
# payload: the raw body bytes you passed to stripe.Webhook.construct_event(...)
try:
requests.post('https://floorsync.towers.app/api/v1/events/stripe', data=payload, timeout=10, headers={
'Content-Type': 'application/json',
'X-Floor-Key': os.environ['FLOORSYNC_KEY'],
'x-amz-content-sha256': hashlib.sha256(payload).hexdigest(),
})
except requests.RequestException:
pass
See what Meta would get, right here
This runs FloorSync’s mapping in your browser. Start with the renewal: it has no browser, so watch what it matches on. Nothing is sent.
Run it on your own pixel, free
A free key runs this dry run against your own Meta pixel and GA4. Just your email, no card. $29 a month per business after 7 free days, only when you start the plan.
Sources: Meta’s Conversions API docs; Tracking Consulting’s Stripe → Meta guide (on {CHECKOUT_SESSION_ID} as event_id and on renewals). Checked November 2026.