Build a fast lead page with Claude Code, GitHub, Vercel and Supabase and send Meta your leads from your own server, or switch it on in Shopify. Every click, every prompt.
Swipe or tap Next · checked on 2026-10-22
Watch the full walkthroughOn Shopify? Stop here and go to If your site is on Shopify. None of this code is for you.
For the custom path you need the basic stack first: Claude Code, a GitHub account and a Vercel account, all logged in on your computer, plus a free Supabase account. Don't have the first three yet? My stack guide walks you through it.
Then open your site's folder, start Claude, and paste the prompt. Claude goes one step at a time and waits for your yes. And the whole time, you paste your keys into files and screens yourself; Claude only ever gets their names.
Start Claude in your site's folder
cd ~/apps/my-site
claudeThe setup prompt (copy all of it) · lines 1-3 of 26. Copy all copies the whole prompt; read it in full on the page.
Hi Claude. Please be my patient setup helper. I run a home-service business and I don't code: one step at a time, tell me exactly what to type or click, and wait for me after each step.
Goal: a fast lead page in my own GitHub, put online by Vercel, that saves every lead in Supabase and sends my Meta conversions from my own server instead of a Facebook pixel in the browser, so ad blockers can't eat them.Hi Claude. Please be my patient setup helper. I run a home-service business and I don't code: one step at a time, tell me exactly what to type or click, and wait for me after each step.
Goal: a fast lead page in my own GitHub, put online by Vercel, that saves every lead in Supabase and sends my Meta conversions from my own server instead of a Facebook pixel in the browser, so ad blockers can't eat them.
Follow this guide. Its sections are the source of truth for every step:
https://receiptsgroup.com/guides/server-side-conversions
If I don't have the stack yet (Claude Code, GitHub, Vercel), stop and send me to https://receiptsgroup.com/guides/own-your-app-stack first. If my site is on Shopify, stop: none of this code is needed, send me to the guide's "If your site is on Shopify" section instead.
Steps:
1. Check the stack: gh auth status and vercel whoami should both show my account. Make sure this folder is a GitHub repo that Vercel deploys.
2. Set up the safe way to change things: from now on every change goes on a new branch, you open a pull request, Vercel builds a preview link, I look at it on my phone, and only I say merge. Write that rule into CLAUDE.md. Never push straight to main.
3. The keys (guide: "Hand the keys to Claude Code safely"): check that .env.local is ignored by git (git check-ignore -v .env.local), and add the deny rule so you can't read .env files. Then tell me which values I need and walk me through where to click for each one (guide: "Get your Meta keys, click by click" and "Save every lead in Supabase"). I paste the values into .env.local and into Vercel myself. You only ever use the NAMES: META_PIXEL_ID, META_CAPI_TOKEN, META_TEST_EVENT_CODE, SUPABASE_URL, SUPABASE_SECRET_KEY. Never ask me to paste a key into this chat, and never open or print .env files.
4. Build the lead page (guide: "Build the page"): static, a short two-question form, then name and mobile, a big call button. No Facebook pixel, no fbevents.js, ever.
5. Supabase (guide: "Save every lead in Supabase"): a leads table with row level security on. Show me the SQL and I run it. Then the lead route saves the lead first and sends Meta the Lead with the same event_id.
6. Server-side events (guide: "Send Meta your events from your own server"): one small API route on my own site that takes a tiny note from the browser and sends it to Meta's Conversions API. Lead only fires from the server route that saves the form. Every event gets an event id.
7. First test (guide: "First test"): META_TEST_EVENT_CODE set on Preview only in Vercel, then Meta's Test events tab while I tap around the preview.
8. Only after the test passes: the bot filter, then the CRM sending booked, showed and sold back to Meta (guide sections of the same names). One pull request each.
9. A day after it's live: help me read Event Match Quality and the deduplication tab (guide: "Check match quality and duplicates").
House rules for this setup:
- If you use a technical word, explain it in plain English the first time.
- Ask me before you assume anything about my business, my accounts or my setup.
- Stop and check with me before anything that costs money or spends credits.
- Stop and check with me before anything that sends, posts or publishes to real people. Merging is going live: only I merge.
- Stop and check with me before you delete or overwrite anything, and before you run anything against my database.
- Never submit my real lead form to test it. A real submission is a real lead and a real conversion in my ads.This is the custom path, the manual way. Every click and prompt, if you'd rather drive it yourself or check what Claude is doing.
The blocker has nothing to block: there's no Facebook script on the page, and the note goes to your own site's address.
Full honesty: my first version went straight to the live site with no pull requests. Don't do that. Since then every change goes the same way, and it's the reason I can change a page that runs paid traffic without sweating it.
Like you're talking to a developer. One change at a time.
It writes what it changed and what it checked in the pull request.
Vercel builds a preview deployment for every pull request and posts the link on it. Open it on your phone.
If it looks right, merge. Merging to your production branch (usually main) is a production deploy: it's live.
Put the rule in CLAUDE.md
## How changes ship
- Never push to main. Every change: a new branch, a pull request, the Vercel preview link.
- Write in the pull request what changed and what you checked.
- Only I merge. Merging is going live.
- Never submit the real lead form to test. Never print .env files or tokens.Prompt: one small change
On a new branch, change <what you want changed> on the lead page. Open a pull request that says what changed and what you checked, give me the Vercel preview link, and wait. Don't merge.What's on mine: the page is static, so it's already built before anybody shows up. The big picture at the top starts downloading with the page itself. The extra tags wait.
The form is two quick taps (what you sell, where), then the contact step, and a name plus a mobile number already counts as a lead, so somebody who bails halfway isn't lost. There's a big call button, and a real tap on it counts as a contact.
After the form, the thank-you page has a walkthrough video and a calendar to book on.
The picture in my hero is a render I made in Higgsfield, background removed there too. Use your own photo or render; just keep it one big image, not a slideshow.
Prompt: the lead page · Part 1 of 2 · Copy copies all 10 lines
On a new branch, build a lead page for <your offer> at /<your-page>. Rules:
- Static: built ahead of time, not when someone visits.
- Preload the one big hero image so it shows first. Nothing render-blocking.
- No Facebook pixel. No fbevents.js, no fbq, no noscript image. Meta events come from the server only (we do that next).
- Any other third-party tag waits for the first tap or a few seconds.On a new branch, build a lead page for <your offer> at /<your-page>. Rules:
- Static: built ahead of time, not when someone visits.
- Preload the one big hero image so it shows first. Nothing render-blocking.
- No Facebook pixel. No fbevents.js, no fbq, no noscript image. Meta events come from the server only (we do that next).
- Any other third-party tag waits for the first tap or a few seconds.
- The form: two quick questions first (<question 1>, <question 2>), then the contact step: name and mobile, then the rest.
- As soon as I have a name and a mobile number, save it as a lead, so a half-finished form isn't lost.
- A big call button.
- After the form: a thank-you page with my calendar embed.
Open a pull request with the preview link and wait.Prompt: the lead page · Part 2 of 2 · Copy copies all 10 lines
- The form: two quick questions first (<question 1>, <question 2>), then the contact step: name and mobile, then the rest.
- As soon as I have a name and a mobile number, save it as a lead, so a half-finished form isn't lost.
- A big call button.
- After the form: a thank-you page with my calendar embed.
Open a pull request with the preview link and wait.Meta moves these menus around and renames things all the time (half its screens say pixel, the other half say dataset, and it's the same number). If a button isn't exactly where I say, look for the same words one screen over. You need two values out of Meta: the pixel (dataset) ID and a Conversions API access token.
You'll see your pixels and datasets, each with its name and its ID. Click the one your ads use.
Copy just the number. That's your META_PIXEL_ID. It isn't a password (a normal pixel puts it right in the page's code), but it still goes in .env.local and Vercel, not in your code, so you can swap it in one place.
Don't have one yet? Events Manager → Connect data → Web → Connect, type a name, Create pixel. That makes the ID. Meta then offers to help you install the pixel code; you can stop there, because we never put the pixel code on the page.
Follow the pop-up. When Meta shows you the token, copy it straight into .env.local and Vercel (next section) as META_CAPI_TOKEN. Don't park it in a note, a text or an email.
Meta's developer docs then say: go to the Overview tab, click Manage integrations, and click Manage next to Conversions API. That sets up the app and the system user behind the token for you, no app review needed.
Read the overview, Continue, pick the events you'll send (for a lead page: Lead, Contact and the booked appointment), pick at least one customer info field, Continue, then Confirm setup. Choose to implement it yourself and click Open implementation guide. From then on it's Manage integrations → Complete setup on the Conversions API → Generate access token. Copy it the same way.
Meta hides that link from anyone without developer access to the business. Two ways around it:
If somebody else owns your business portfolio (an agency, a partner), sort this out before you start building, not halfway through.
Here's the rule I run, and I'd never bend it: Claude Code gets the names of my keys, never the values. The values live in exactly two places, a file called .env.local on my computer and Vercel's Environment Variables screen, and I paste them in myself. Claude writes code that reads META_CAPI_TOKEN by name, and it works without Claude ever seeing what's in it.
META_PIXEL_IDMETA_CAPI_TOKENMETA_TEST_EVENT_CODESUPABASE_URLSUPABASE_SECRET_KEYFive values, five names. Claude only ever sees the right-hand column.
In your site's folder, run the check below. If it prints a line that mentions .gitignore, you're good. If it prints nothing, the file isn't ignored yet: add the two lines to .gitignore before you put a single value in it.
Then run the second command. If it prints .env.local, that file was already committed at some point: treat every key that was ever in it as leaked and rotate them (below).
Is it ignored?
git check-ignore -v .env.localAdd to .gitignore if it isn't
.env
.env*.localWas it ever committed?
git ls-files .env.localOpen .env.local in a plain text editor (VS Code, or TextEdit switched to plain text), not inside the Claude chat. One line each, NAME=value, no spaces around the equals sign. Swap each placeholder for the real value and save.
.env.local (placeholders: put your own values in)
META_PIXEL_ID=your-pixel-or-dataset-id
META_CAPI_TOKEN=paste-your-access-token-here
META_TEST_EVENT_CODE=
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SECRET_KEY=paste-your-secret-key-hereFor each one: type the Name, paste the Value, pick the environments (Production, Preview, Development), Save. Vercel now asks you to choose Config or Secret: the token and the Supabase secret key are Secret (write-only once saved, nobody can read them back), the pixel ID and the Supabase URL can be Config. META_TEST_EVENT_CODE goes on Preview only.
A change only applies to new deployments, so redeploy after you add or change anything.
Belt and braces: a deny rule in your project's Claude Code settings, so it can't open .env files even if it tries. The file holds no secrets, so it's fine to commit.
.claude/settings.json
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)"
]
}
}The prompt: give Claude the names, never the values · Part 1 of 2 · Copy copies all 4 lines
My keys are already in .env.local on this computer and in Vercel's Environment Variables. Don't open, print, copy or ask me for any of them. Read these from the environment by NAME only, and only in server code:
META_PIXEL_ID, META_CAPI_TOKEN, META_TEST_EVENT_CODE (set on Preview only, may be empty), SUPABASE_URL, SUPABASE_SECRET_KEY.My keys are already in .env.local on this computer and in Vercel's Environment Variables. Don't open, print, copy or ask me for any of them. Read these from the environment by NAME only, and only in server code:
META_PIXEL_ID, META_CAPI_TOKEN, META_TEST_EVENT_CODE (set on Preview only, may be empty), SUPABASE_URL, SUPABASE_SECRET_KEY.
None of these ever goes in browser code, so never give any of them a NEXT_PUBLIC_ prefix. Never log them, never put them in an error message, a commit, a pull request, a test file or a URL. If one is missing, tell me which NAME is missing and stop; I'll add it myself.
Add a .env.example with the names and empty values so the next person knows what's needed.The prompt: give Claude the names, never the values · Part 2 of 2 · Copy copies all 4 lines
None of these ever goes in browser code, so never give any of them a NEXT_PUBLIC_ prefix. Never log them, never put them in an error message, a commit, a pull request, a test file or a URL. If one is missing, tell me which NAME is missing and stop; I'll add it myself.
Add a .env.example with the names and empty values so the next person knows what's needed.Swap it first, then kill the old one, in that order, so the page never goes down.
.env.local and Vercel, redeploy, then have whoever has full control of your business portfolio remove the old one.On my page, the form posts to one server route. That route saves the lead in Supabase first, with a key that only the server has, and only then sends Meta the Lead. So the lead is in my own database even if Meta has a bad day, and the Lead event comes from the same place the form data is. The browser never talks to Supabase at all.
New projects get a publishable key (starts sb_publishable_) and a secret key (starts sb_secret_). The old anon and service_role keys still work, but Supabase says it's deprecating them by the end of 2026. My build still runs on the older service_role key; if you're starting now, use the secret key. Same rule either way: the secret one is server only. If a tutorial or an AI tells you to copy a long key starting eyJ, it was written for the old keys.
Pick your organization, give the project a name, set a database password (save it in your password manager; it's not one of the keys below and you won't need it for this page), and pick the region closest to your customers. Wait for it to finish setting up.
Copy the project URL (it looks like https://your-project.supabase.co) and the secret key. If the project has no secret key yet, create one on that API Keys screen. You don't need the publishable key for this page, because only your server talks to Supabase.
As SUPABASE_URL and SUPABASE_SECRET_KEY, the same way as the Meta keys. The secret key is a Vercel Secret.
Paste the prompt below. Claude writes the SQL; you look at it and run it yourself in the next step.
Paste the SQL Claude wrote and click Run. Then open the Table Editor and check the leads table is there. With row level security on and no rules added, nothing gets in through a publishable key; your server's secret key still can.
Last prompt in this section: the lead route saves the row, then sends Meta the Lead with the same event_id it just saved. Later, the CRM sends Schedule back with the ids that ride along with that lead, so Meta ties it all together.
Prompt: the leads table · Part 1 of 2 · Copy copies all 9 lines
On a new branch, add a Supabase migration (a SQL file in supabase/migrations) that creates a leads table:
- id (uuid, default gen_random_uuid()) and created_at (timestamptz, default now())
- the form fields: name, phone, email, plus one column for each quiz question: <question 1>, <question 2>
- event_id (text, unique): the same id we send Meta for this Lead
- fbc and fbp (text): Meta's click id and browser id, stored as they are, not hashedOn a new branch, add a Supabase migration (a SQL file in supabase/migrations) that creates a leads table:
- id (uuid, default gen_random_uuid()) and created_at (timestamptz, default now())
- the form fields: name, phone, email, plus one column for each quiz question: <question 1>, <question 2>
- event_id (text, unique): the same id we send Meta for this Lead
- fbc and fbp (text): Meta's click id and browser id, stored as they are, not hashed
- page_url and the utm fields from the ad link
- is_test (boolean, default false)
In the same migration, turn on row level security for the table and add no policies, so only our server (with the secret key) can read or write it. Revoke the anon and authenticated grants on it, and make sure service_role keeps its grants.
Show me the SQL and wait. Don't run anything against my database; I'll run it in the SQL Editor myself.Prompt: the leads table · Part 2 of 2 · Copy copies all 9 lines
- page_url and the utm fields from the ad link
- is_test (boolean, default false)
In the same migration, turn on row level security for the table and add no policies, so only our server (with the secret key) can read or write it. Revoke the anon and authenticated grants on it, and make sure service_role keeps its grants.
Show me the SQL and wait. Don't run anything against my database; I'll run it in the SQL Editor myself.Prompt: save the lead, then send the Lead · lines 1-3 of 7. Copy all copies the whole prompt; read it in full on the page.
On a new branch, change the lead route so that on every form submit (and on the name + mobile step):
1. Make the event_id on the server from the phone number and the day, so the same person sending the form twice that day is still one Lead in Meta.
2. Keep the fbclid from the ad link. If there's no _fbc cookie, build fbc in Meta's format: fb.1.<time in milliseconds>.<fbclid>. fbp only exists if a cookie is there; with no pixel it's often blank, and that's fine.On a new branch, change the lead route so that on every form submit (and on the name + mobile step):
1. Make the event_id on the server from the phone number and the day, so the same person sending the form twice that day is still one Lead in Meta.
2. Keep the fbclid from the ad link. If there's no _fbc cookie, build fbc in Meta's format: fb.1.<time in milliseconds>.<fbclid>. fbp only exists if a cookie is there; with no pixel it's often blank, and that's fine.
3. Insert the row into leads with a Supabase client made from SUPABASE_URL and SUPABASE_SECRET_KEY, in server code only.
4. Then send Meta the Lead with the same event_id: hashed email and phone, fbc and fbp unhashed, action_source website.
5. If the insert fails, still send the Lead, log the failure without any personal data, and tell me how you'll alert me. The visitor always gets the thank-you, never an error.
Never import the Supabase secret client in any file the browser loads. Open a pull request with the preview link and wait.In a Next.js site, anything whose name starts with NEXT_PUBLIC_ gets shipped to every visitor's browser. If Claude ever writes NEXT_PUBLIC_SUPABASE_SECRET_KEY or NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY, stop it right there. That's your whole database handed to anybody who opens the page.
You need META_PIXEL_ID and META_CAPI_TOKEN in .env.local and in Vercel. Haven't done that yet? Go back to Get your Meta keys, click by click and Hand the keys to Claude Code safely.
Paste the prompt below. It builds one small route on your own site that takes a tiny note from the browser and sends the event to Meta. The Lead itself comes from the lead route you set up in the Supabase section.
Ask Claude to show you where each key is read. Every one should be in server code, read by name from the environment, and none of them should start with NEXT_PUBLIC_.
META_PIXEL_IDYour pixel (dataset) id: where the server events go
Events Manager → Data sources → your datasetMETA_CAPI_TOKENThe Conversions API access token. Secret: server only, never in browser code
Your dataset → Settings → Conversions API → Generate access tokenMETA_TEST_EVENT_CODEMeta's test code, so test events show up live. Set on Preview only
Your dataset → Test events tabSUPABASE_URLYour Supabase project's address
Supabase → your project → ConnectSUPABASE_SECRET_KEYSupabase's secret key (older projects: service_role). Server only
Supabase → Settings → API KeysPrompt: server-side events · lines 1-2 of 10. Copy all copies the whole prompt; read it in full on the page.
On a new branch, add server-side Meta events. No Facebook pixel in the browser.
1. A small API route on our own site, for example /api/meta-event. The browser sends it a tiny note (event name, an event id, the page address). The route sends the event to Meta's Conversions API: POST https://graph.facebook.com/<API version>/<META_PIXEL_ID>/events, with META_CAPI_TOKEN from the environment.On a new branch, add server-side Meta events. No Facebook pixel in the browser.
1. A small API route on our own site, for example /api/meta-event. The browser sends it a tiny note (event name, an event id, the page address). The route sends the event to Meta's Conversions API: POST https://graph.facebook.com/<API version>/<META_PIXEL_ID>/events, with META_CAPI_TOKEN from the environment.
2. The browser may only send these through that route: ViewContent, InitiateCheckout, and Contact (only on a real tap on the call button, once per page view). Lead and Schedule are never accepted from the browser.
3. Lead fires from the server route that saves the form to Supabase, after the save, where the form data is, so nobody can fake a conversion from a browser.
4. Every event gets an event_id so Meta counts it once.
5. Send what Meta's parameters page asks for on website events: action_source website, event_source_url, client_user_agent and the IP. Hash the contact fields Meta says to hash; send fbc and fbp unhashed.
6. Keep the ad click id (fbclid) when a visitor moves to another page of the site.
7. If META_TEST_EVENT_CODE is set, add it as test_event_code at the top level of the request (next to data, not inside an event). If it's empty, send nothing.
8. Never log or print the token. Read it from the environment only, in server code only.
Open a pull request with the preview link and wait.There's no Facebook script on the page, so there's nothing for a blocker to kill, and the note goes to your own site's address. Leads only come from the server, where the form data really is. On my own campaigns, conversions felt like they were just disappearing before I did this. That's why I built it.
If you picked a dataset, choose website events. Meta's developer docs call this a test ID, and you send it as test_event_code. Copy it.
META_TEST_EVENT_CODETick Preview only, save, and redeploy the preview. That way the test code can never reach your live site, and there's nothing to remember to delete before you merge.
Open the preview link, scroll, start the quiz, tap the call button. Don't submit your real form with your real details. To test a Lead, have Claude send one made-up test lead from the server (the prompt below), marked as a test.
Meta says server events show up in the Test events window once the request is sent. Click one to see what was in it: the event name, the event id, which customer fields came through. The test activity stays there for 24 hours or until you click Clear activity.
Meta warns that events sent with a test code aren't thrown away: they flow into Events Manager and get used for targeting and measurement. So one or two test Leads, not twenty. Then delete the test rows from your leads table.
Prompt: the test
META_TEST_EVENT_CODE is now set in Vercel for Preview only. Confirm the event route adds it as test_event_code only when it's set, and that production can never send it. Then give me a way to send one made-up test Lead from the server on this preview (name Test Lead, a 555 number), saved to leads with is_test = true and not sent to my CRM. Tell me when the preview is ready. After I confirm the events show in Test events, show me how to delete the test rows. Don't merge.META_TEST_EVENT_CODE is now set in Vercel for Preview only. Confirm the event route adds it as test_event_code only when it's set, and that production can never send it. Then give me a way to send one made-up test Lead from the server on this preview (name Test Lead, a 555 number), saved to leads with is_test = true and not sent to my CRM. Tell me when the preview is ready. After I confirm the events show in Test events, show me how to delete the test rows. Don't merge.My leads dropped way off with the exact same ads, so I went digging. Two kinds of traffic were feeding Meta junk:
{{adset.name}}), the blank that's supposed to get filled in with the ad's name.Now those get a polite answer from my server and nothing is passed to Meta, so the ad sets learn from real people only. I'm not claiming the bots caused the whole drop. They were junk either way.
Prompt: the bot filter
On a new branch, add a bot filter to the server event route. Before an event goes to Meta, skip it (answer ok, send nothing) when:
- the browser says it is headless;
- the page address still has an unfilled ad placeholder like {{adset.name}} in it;
- it's a desktop Linux browser sending a page view that never came from a real tap.
Never skip Lead: that only comes from a real form. Log the reason we skipped, without any personal data. Open a pull request with the preview link and wait.On a new branch, add a bot filter to the server event route. Before an event goes to Meta, skip it (answer ok, send nothing) when:
- the browser says it is headless;
- the page address still has an unfilled ad placeholder like {{adset.name}} in it;
- it's a desktop Linux browser sending a page view that never came from a real tap.
Never skip Lead: that only comes from a real form. Log the reason we skipped, without any personal data. Open a pull request with the preview link and wait.The lead is only the start. When my team books the appointment, the CRM sends that back to Meta as a Schedule event with the lead's original event id, so Meta counts one. Then whether they showed up, and whether they bought (a Purchase). The Lead goes to Google's conversions from the server too.
Prompt: the CRM events
On a new branch, add a server route my CRM can call when a lead changes stage (use a shared secret from the environment; never print it). For each stage send Meta a server event: appointment booked = Schedule (reuse the event id our page already gave that lead's Schedule, so Meta counts one), showed up = a custom event, sold = Purchase. Match the lead by the hashed phone and email we already store. Tell me which CRM setting I need to point at the route. Open a pull request with the preview link and wait.On a new branch, add a server route my CRM can call when a lead changes stage (use a shared secret from the environment; never print it). For each stage send Meta a server event: appointment booked = Schedule (reuse the event id our page already gave that lead's Schedule, so Meta counts one), showed up = a custom event, sold = Purchase. Match the lead by the hashed phone and email we already store. Tell me which CRM setting I need to point at the route. Open a pull request with the preview link and wait.If your site runs on Shopify, skip every line of code above. Meta's official connection for Shopify, the Facebook & Instagram sales channel (Shopify's help calls it Facebook and Instagram by Meta), turns the Conversions API on for you once you pick the right data sharing level. It's click, click, done.
It's a sales channel, not an app, so afterward it lives under Sales channels in the left menu, not under Settings → Apps.
Sign in to Facebook, connect the business assets it asks for (your business portfolio, your Page, your ad account, your pixel), accept the terms, Finish setup.
Turn on customer data sharing, then under choose level pick Enhanced or Maximum. Shopify's help says both of those use the Conversions API on top of the pixel, server to server, where browser ad blockers can't block it. Standard is the pixel only, so it's the one you don't want. Meta's own help says the same: Enhanced or Maximum.
Both of those share your customers' name, location, email and phone with Meta for matching, so Shopify says to keep your privacy policy up to date.
If no pixel is connected yet, click Connect next to the one your ads use (or Create new), then Confirm. Meta's help also says to set data access to Always on (or Optimized) in your Shopify settings, and recommends Always on.
theme.liquid, and Online Store → Preferences → Meta pixelIf somebody pasted Meta's pixel code into your theme years ago, delete it (it sits between <head> and </head>) and Save. If there's one in Preferences, click Change, delete it, Save. Otherwise you've got two pixels and doubled numbers.
Look at the connection method on Purchase: it should show the Conversions API (server), not only the browser. Give it a day of real orders, then do Check match quality and duplicates, same screens.
You're using their integration, so you get what Shopify and Meta decided to send. Shopify's help only promises the purchase event server to server; you don't pick the event ids, you can't add your own bot filter, and you can't write your own rules for what reaches Meta. For a store that mostly cares about purchases, that's fine. For a lead business that lives on form fills and calls, that's exactly why I built my own.
Prompt: walk me through the Shopify setup (Claude or ChatGPT) · Part 1 of 2 · Copy copies all 5 lines
Be my patient setup helper. I run a business on Shopify and I don't code. Goal: Meta's Conversions API turned on through Shopify's Facebook & Instagram sales channel, so my purchase events reach Meta server to server.
Use the "If your site is on Shopify" section of this guide as the source of truth: https://receiptsgroup.com/guides/server-side-conversions#shopifyBe my patient setup helper. I run a business on Shopify and I don't code. Goal: Meta's Conversions API turned on through Shopify's Facebook & Instagram sales channel, so my purchase events reach Meta server to server.
Use the "If your site is on Shopify" section of this guide as the source of truth: https://receiptsgroup.com/guides/server-side-conversions#shopify
Go one click at a time and wait for me after each step. Shopify and Meta move their menus around, so if I say a button isn't there, help me find the same words nearby instead of guessing.
No code is needed on this path. Never ask me for a password, a token or a key; none are needed here.
At the end, walk me through checking Events Manager for server events on Purchase.Prompt: walk me through the Shopify setup (Claude or ChatGPT) · Part 2 of 2 · Copy copies all 5 lines
Go one click at a time and wait for me after each step. Shopify and Meta move their menus around, so if I say a button isn't there, help me find the same words nearby instead of guessing.
No code is needed on this path. Never ask me for a password, a token or a key; none are needed here.
At the end, walk me through checking Events Manager for server events on Purchase.Once real traffic is coming in, give it a day, then check these. Meta scores match quality on the last 48 hours of events, so a page with no traffic yet shows you nothing. Same screens on the custom path and on Shopify.
Your events are listed with how many came in. The connection method tells you how each one got there; with my setup it's the Conversions API, since there's no pixel at all. Meta says to allow up to 20 minutes after an event is sent.
You get tabs: Event Overview, Event Coverage, Event Matching (Meta's pages also call it Event Quality), Event Deduplication and Data Freshness.
It's a 0 to 10 score for how well your events match real Meta accounts, and it only exists for website events sent with action_source website. Meta's developer docs say to aim for 6.0 or higher. Clicking the score shows Meta's own suggestions.
What moves it: Meta rates email and the click id (that's fbc) as high priority, and phone, external id and the browser id (fbp) as medium. So send the hashed email and phone you already have on every Lead, plus the fbc from the ad click. Sending a field doesn't guarantee a match, though; plenty of people just don't have a Meta account.
Meta merges two events into one when the event id and the event name match and they arrive within 48 hours of each other.
That matters when you run a pixel and the server together: the tab shows what share of events carry a dedupe key and how much overlaps between browser and server, and higher is better.
I went server only, so there's nothing to merge; my job is never sending the same Lead twice, which is why the event id comes from the phone number and the day.
The delay between the event happening and Meta getting it. Real time is what you want, and a server route that fires right after the save is real time.
Check the Meta variables are set in Vercel for the Preview environment too, including META_TEST_EVENT_CODE, then make a new deployment (a variable change needs a new build). Ask Claude to read the route's logs for Meta's error message.
Meta only shows it to people with developer access to the business. Have whoever has full control send you the setup instructions (that invites you as a developer), or add you to the dataset. Both are in "Get your Meta keys, click by click".
The same event is going out twice with different ids. Every event needs one event id, and the browser and server must never both send the Lead.
Supabase checks the table's grants before row level security, and a missing grant gives a permission error even for the secret key. Ask Claude to check the leads table still grants access to service_role.
That's row level security doing its job: no rule lets that key in. Your server is probably using the publishable key where it needs the secret key.
Supabase blocks secret keys in a browser. If you hit this, the key is in browser code: move it to the server and rotate it.
Treat it as leaked. Make a new one, swap it into .env.local and Vercel, redeploy, then kill the old one ("If a key leaks anyway").
Check data sharing is on Enhanced or Maximum, not Standard (Standard is the pixel only), and that the pixel your ads use is the one connected.
Send more customer info with each event: hashed email and phone, and the fbc from the ad click. Click the score in Events Manager for Meta's own fixes.
Look at the skipped-reason log for a pattern (a placeholder in the link, a headless browser) and add it to the bot filter as its own pull request.
In Vercel, roll back to the last good production deployment, then fix it on a branch with a preview like any other change.
What each page backs up is listed at the end of the full guide.
On a rented site builder you're stuck with their pixel and whatever they decide to show you. When the code sits in your own GitHub, you decide what Meta hears, and Meta's own bots don't get a vote.
Want help setting this up? Receipts Group builds these systems.
Open the full guide as a page (every file in full, plus the table of contents).
Watch the full walkthrough