Tutorialthe whole build, step by step

Server-side Meta conversions for a lead page, built with Claude Code

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.

By Eric Snyder, founder 22 min readChecked on Oct 22, 2026
Save this guidereceiptsgroup.com/guides/server-side-conversions
You buildA lead page with a short form and a call button, every lead saved in Supabase, and server-side Meta events
You needClaude Code (a paid Claude plan), GitHub, Vercel, Supabase, and developer access to your Meta pixel (dataset) in Events Manager
On Shopify?No code. Meta's Facebook & Instagram channel with data sharing on Enhanced or Maximum turns the Conversions API on
You don't needTo write code. You describe the change, Claude Code writes it, you check a preview link and merge
The honest limitThis is about Meta. Google's tag can still load in the browser; the guide sends the Lead to Google from the server too
In this guide
Start here
  1. Start here: paste this into Claude Code
The manual way
  1. How the pieces connect
Custom site with Claude Code
  1. How every change gets made
  2. Build the page
  3. Get your Meta keys, click by click
  4. Hand the keys to Claude Code safely
  5. Save every lead in Supabase
  6. Send Meta your events from your own server
  7. First test: Meta's Test events tab
  8. Keep Meta's own bots out of your ad data
  9. Send booked, showed and sold back to Meta
On Shopify
  1. If your site is on Shopify
Both paths
  1. Check match quality and duplicates
  2. When it breaks
  3. Sources, checked Oct 22, 2026

A Facebook pixel lives in your visitor's browser, and ad blockers and pixel blockers kill it. So somebody fills out your form, you paid for the click, and Meta never hears about it. Then it optimizes your ads on bad data.

The fix is to send your events from your own server. This guide is how I do it: a fast lead page in my own GitHub, put online by Vercel, built by talking to Claude Code, every lead saved in Supabase, and no Meta pixel on the page at all. You get every click in Meta's screens, how to hand your keys to Claude Code without leaking them, the prompts, and the bot filter that keeps Meta's own review traffic out of your ad data.

Which path is yours?

  • Your own site, or you want one: the custom path. Start with the prompt below and Claude Code walks you through every section.
  • Your site runs on Shopify: you don't need any code. Jump to If your site is on Shopify, it's a few clicks.

Either way, finish with Check match quality and duplicates once real traffic is coming in.

Checked on Oct 22, 2026

Start herepart 1

Start here: paste this into Claude Code

On 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.

bashStart Claude in your site's folder
cd ~/apps/my-site
claude
PromptThe setup prompt (copy all of it)
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.
The manual waypart 2

How the pieces connect

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.

  1. 1
    Your visitor's browserYour lead page. No Facebook pixel, no Facebook script→ a tiny note to your own site: page viewed, form started, call button tapped, form sent
  2. 2
    Your own serverOne small API route on your site (Vercel runs it, your keys live here)→ the lead, saved first, and then the event, with an id
  3. 3
    SupabaseYour database. The server saves every lead here with a server-only key→ the lead row, holding the same event id Meta is about to get
  4. 4
    Meta's Conversions APIMeta's way of taking events straight from a server→ conversions an ad blocker never saw, counted once
  5. 5
    Your CRMWhere your team tracks every lead→ booked, showed up, sold: sent back to Meta with the lead's original id

The blocker has nothing to block: there's no Facebook script on the page, and the note goes to your own site's address.

Words you'll see

  • Pixel / dataset: the pixel is Meta's tracking code that runs in the visitor's browser. Meta now files your events under a dataset, and if your pixel got converted, the dataset ID is the same number as the pixel ID. That number is still where your server events go.
  • Conversions API (CAPI): Meta's way of taking events from your server instead of the browser.
  • Access token: the password your server uses to send events to Meta. Secret.
  • Event id: a label on each event so Meta counts it once, not twice.
  • Test event code: a code from Meta's Test events tab that makes your events show up live while you test.
  • Event Match Quality: Meta's 0 to 10 score for how well your events match real Meta accounts.
  • Supabase: a hosted database. Think of a spreadsheet your code reads and writes, with real locks on it.
  • Secret key (older name: service_role): Supabase's master key. It skips every lock, so it lives on the server only.
  • Row level security (RLS): Supabase's locks. On, with no rules added, nobody gets in except your server.
  • Environment variable: a named setting (like META_CAPI_TOKEN) your code reads at run time, so the value never sits in your code.
  • .env.local: the file on your computer that holds those values for local work. Git ignores it, so it never goes to GitHub.
  • Branch / pull request: a branch is a copy of the code to work on; a pull request is the proposed change you look at before it goes live.
  • Preview: a private link Vercel builds for each pull request, so you can check the change on your phone.
  • Merge: accepting the pull request. On Vercel, merging to main is going live.
Custom site with Claude Codeparts 3 to 11

How every change gets made

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.

  1. Tell Claude Code what you want, in plain English

    Like you're talking to a developer. One change at a time.

  2. Claude makes a branch and opens a pull request

    It writes what it changed and what it checked in the pull request.

  3. Vercel builds a preview

    WhereThe pull request page on GitHub

    Vercel builds a preview deployment for every pull request and posts the link on it. Open it on your phone.

  4. You merge

    If it looks right, merge. Merging to your production branch (usually main) is a production deploy: it's live.

markdownPut 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.
PromptPrompt: 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.

Build the page

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.

PromptPrompt: the lead page
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.

Get your Meta keys, click by click

  1. Open Events Manager

    Wherebusiness.facebook.com → Events Manager → Data sources in the left menu (some accounts say Datasets)

    You'll see your pixels and datasets, each with its name and its ID. Click the one your ads use.

  2. Copy the pixel (dataset) ID

    WhereData sources → your pixel or dataset → the ID number shown with its name

    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.

  3. Make the Conversions API access token

    WhereData sources → your pixel → Settings tab → Conversions API section → Generate access token (under Set up manually)

    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.

    PermissionsOnly people with developer access to the business see the Generate access token link. Anyone holding the token can send events to your dataset, so it's a password.
  4. Or, if Meta shows you the guided setup instead

    WhereEvents Manager → Datasets → your dataset → Add Events → Add new integration → Conversions API → Set up → Set up manually → Next

    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.

  5. Can't see Generate access token?

    Meta hides that link from anyone without developer access to the business. Two ways around it:

    • Get invited. Whoever has full control of your business portfolio runs the guided setup above and clicks Send instructions with your email. Meta emails you an invite to accept developer access, and then you finish under Manage integrations.
    • Get added to the dataset. They go to Meta Business Suite Settings → your business portfolio → Data sources → Datasets → the dataset → Add people, pick you, set your access, Assign. You have to be in the business portfolio first.

    If somebody else owns your business portfolio (an agency, a partner), sort this out before you start building, not halfway through.

Hand the keys to Claude Code safely

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.

Five values, five names. Claude only ever sees the right-hand column.
ValueWhere you get itSecret?Name it
Pixel (dataset) IDEvents Manager → Data sources → your datasetNo, but keep it out of your codeMETA_PIXEL_ID
Conversions API access tokenYour dataset → Settings → Generate access tokenYes. Server onlyMETA_CAPI_TOKEN
Test event codeYour dataset → Test events tabNo. Preview onlyMETA_TEST_EVENT_CODE
Supabase project URLSupabase → your project → ConnectNoSUPABASE_URL
Supabase secret keySupabase → Settings → API KeysYes. Server only, skips every lockSUPABASE_SECRET_KEY
  1. Check that git ignores .env.local

    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).

    bashIs it ignored?
    git check-ignore -v .env.local
    Add to .gitignore if it isn't
    .env
    .env*.local
    bashWas it ever committed?
    git ls-files .env.local
  2. Fill in .env.local yourself

    Open .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-here
  3. Put the same values in Vercel

    WhereVercel → your project → Settings → Environment Variables

    For 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.

  4. Lock .env files away from Claude Code

    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.

    json.claude/settings.json
    {
      "permissions": {
        "deny": [
          "Read(./.env)",
          "Read(./.env.*)"
        ]
      }
    }
PromptThe prompt: give Claude the names, never the values
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.

If a key leaks anyway

Swap it first, then kill the old one, in that order, so the page never goes down.

  • Meta access token: make a new one in the same spot (Settings → Generate access token), put it in .env.local and Vercel, redeploy, then have whoever has full control of your business portfolio remove the old one.
  • Supabase secret key: Supabase's own order is: create a new secret key in Settings → API Keys, replace it everywhere, check everything works, then delete the old one (deleting can't be undone). On an older project with the service_role key, you deactivate the legacy keys in that same screen instead.
  • It got committed to GitHub: deleting it in a new commit doesn't take it out of the history. Rotate it like it's public, because it is.

Save every lead in Supabase

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.

  1. Create the project

    Wheresupabase.com/dashboard → New project

    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.

  2. Find the project URL and the secret key

    WhereYour project → Connect (project URL), and Settings → API Keys (every key)

    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.

    PermissionsThe secret key skips every row level security rule. Supabase blocks it from working in a browser (it answers 401), but a leaked one still works from anywhere else.
  3. Put both in .env.local and Vercel

    As SUPABASE_URL and SUPABASE_SECRET_KEY, the same way as the Meta keys. The secret key is a Vercel Secret.

  4. Have Claude write the leads table

    Paste the prompt below. Claude writes the SQL; you look at it and run it yourself in the next step.

  5. Run the SQL yourself

    WhereSupabase → your project → SQL Editor

    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.

  6. Save the lead, then fire the Lead

    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.

PromptPrompt: the leads table
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 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.
PromptPrompt: save the lead, then send the Lead
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.

Send Meta your events from your own server

  1. Keys first

    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.

  2. Have Claude build the server route

    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.

  3. Check the pull request before you merge

    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_ID

Your pixel (dataset) id: where the server events go

Where: Events Manager → Data sources → your dataset
META_CAPI_TOKEN

The Conversions API access token. Secret: server only, never in browser code

Where: Your dataset → Settings → Conversions API → Generate access token
META_TEST_EVENT_CODE

Meta's test code, so test events show up live. Set on Preview only

Where: Your dataset → Test events tab
SUPABASE_URL

Your Supabase project's address

Where: Supabase → your project → Connect
SUPABASE_SECRET_KEY

Supabase's secret key (older projects: service_role). Server only

Where: Supabase → Settings → API Keys
PromptPrompt: server-side events
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.

First test: Meta's Test events tab

  1. Get the test code

    WhereEvents Manager → Data sources → your dataset → Test events tab → the server events section

    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.

  2. Put it in Vercel, Preview only

    WhereVercel → your project → Settings → Environment Variables → META_TEST_EVENT_CODE

    Tick 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.

  3. Tap around the preview on your phone

    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.

  4. Watch them land

    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.

  5. Keep it to a couple

    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.

PromptPrompt: 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.

Keep Meta's own bots out of your ad data

My leads dropped way off with the exact same ads, so I went digging. Two kinds of traffic were feeding Meta junk:

  • Meta's own ad-review browser. It opened every new ad's link on a desktop Linux computer, sat there, never typed a thing, and not one of those visits became a lead.
  • A click-everything crawler. It walked through my quiz and tapped the call button, and every time it came in on an ad link with Meta's placeholder still in it (like {{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.

PromptPrompt: 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.

Send booked, showed and sold back to Meta

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.

PromptPrompt: 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 Shopifypart 12

If your site is on Shopify

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.

Before you start

  • A Shopify online store that isn't in private mode, with a real sender email set.
  • A Facebook account, and a published Facebook Page for the business.
  • A business portfolio connected to that Page. Your own Facebook account needs full control of both the portfolio and the Page.
  • An ad account in that portfolio if you're going to run ads.
  1. Add the channel

    WhereShopify admin → Settings → Sales channels → Shopify App Store → search Facebook → Add channel (or Add app)

    It's a sales channel, not an app, so afterward it lives under Sales channels in the left menu, not under Settings → Apps.

  2. Connect your Meta account

    WhereStart set up → Connect account

    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.

  3. Turn on data sharing at the level that includes the Conversions API

    WhereSales channels → Facebook & Instagram → Settings → Data sharing settings (some of Shopify's pages call it Share data settings)

    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.

  4. Connect the right pixel

    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.

  5. Take out any old pixel code

    WhereOnline Store → current theme → … → Edit code → theme.liquid, and Online Store → Preferences → Meta pixel

    If 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.

  6. Confirm Meta is getting server events

    WhereEvents Manager → Data sources → the pixel Shopify uses → Overview

    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.

PromptPrompt: walk me through the Shopify setup (Claude or ChatGPT)
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#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.
Both pathsparts 13 to 14

Check match quality and duplicates

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.

  1. See the server events arriving

    WhereEvents Manager → Data sources → your dataset → Overview

    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.

  2. Open an event's details

    WhereOverview → find the event (say, Lead) → View details

    You get tabs: Event Overview, Event Coverage, Event Matching (Meta's pages also call it Event Quality), Event Deduplication and Data Freshness.

  3. Read Event Match Quality

    WhereOverview → click the Event Match Quality score

    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.

  4. Check deduplication

    WhereView details → Event Deduplication

    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.

  5. Glance at freshness

    WhereView details → Data Freshness

    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.

When it breaks

No events in Test events
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.
You don't see Generate access token
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".
Meta counts a lead twice
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 says permission denied
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.
Supabase answers with no rows and no error
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.
The Supabase secret key gets a 401
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.
A key ended up in the chat, a commit or a screenshot
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").
Shopify: no server events in Events Manager
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.
Event Match Quality is low
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.
Junk events keep reaching Meta
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.
A change broke the live page
In Vercel, roll back to the last good production deployment, then fix it on a branch with a preview like any other change.

Own the plumbing

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.

Checked on Oct 22, 2026 against

Every claim about a third-party tool in this guide (plans, prices, menu paths, commands, limits) was checked against these official pages on Oct 22, 2026. These screens change often: if something looks different, trust the page over this guide.

Rather skip the setup?

Want help setting this up?

Receipts Group builds these systems. Thirty minutes with Eric tells you which piece is worth building first, or that none of them are. The guides stay free either way.

Keep going

more free guides