Dog Academy × Meta: tracking the leads that turn into sales
1. Summary
We want Meta to find people who book a free consultation and go on to buy Trained for Life, not people who just fill in a form. Meta can only do that if it can see what happens after someone clicks an ad. Right now it can't.
Where tracking stands today
We submitted one labelled test lead through the quiz on 28 September and watched every request. Here's what we found:
- The quiz sends Meta nothing. No Meta event fired at any step our test lead reached, through the email step. The phone check stopped it before the final submit. Google Analytics gets "Step 1, Step 2…", but Meta only gets page views and automatic button clicks.
- There is no server-side tracking (Conversions API). Everything depends on the browser pixel. Ad blockers, iPhones and cookie limits drop a share of those events.
- The pixel is missing from some pages: the private, group and online program pages, and the Heyflow city funnels on training.dogacademy.org. The Facebook click ID is only kept if the visitor lands straight on the booking form.
- Meta is matching the wrong name. Its automatic matching reads your dog's-name field as the owner's last name, which lowers how well Meta can match leads to real people.
- Sales happen on the phone, days after the click, and nothing tells Meta about them.
What we're proposing
- Fix the pixel. Put it on every page that matters, take it off the service-dog and psychiatric pages, and stop the name mix-up.
- Add the Conversions API. Your server sends the same events as the browser, with a shared ID so nothing counts twice.
- Track three website events properly: Lead (quiz submitted), Schedule (free consultation booked) and Purchase (Trained for Life paid on your site).
- Take payment on your site. Reps close on the call and send a pay link, so the customer pays on dogacademy.org. That is what lets Meta optimize to buyers.
- Feed the later stages from your own systems: consultation completed, purchase and, optionally, "sale kept" after the refund window. This goes through a small secure endpoint, a daily file or Zapier.
- Test everything with Meta's test tools and one real end-to-end test lead. Then we launch ads optimized to Purchase, the sale.
Time: the programming is about one developer day, plus the pay link if you don't already have an online checkout. Launch-ready in about 3–4 days, because Meta needs 48 hours of real traffic before it can score match quality. Nothing in this plan changes what your leads see, and none of the tracking code is allowed to break the quiz or the booking page.
2. What we're trying to accomplish, and why it's set up this way
The goal: leads that buy
Meta's delivery system learns from whatever event we tell it to chase. If we tell it "form filled", it finds form-fillers: people who get curious, never book and never answer the phone. We have $10k to find winners, so every learning event has to point at buyers.
Why the sale has to be paid on your site
- We optimize to the sale. The ads are told to find people who buy Trained for Life, not people who fill in the quiz or book a call.
- Meta can only optimize to what happens on your website, within 7 days of the ad click. A card taken over the phone and logged in your CRM can be reported to Meta, but it can't train the ads.
- So the rep closes on the call and sends a pay link. The customer pays on dogacademy.org. Your site fires Purchase from the browser and the server, and that is the event the campaign optimizes to.
- Expect "learning limited" at first. Meta learns fastest at about 50 sales a week per ad set, and a test budget produces far fewer. It still steers toward buyers, just more slowly.
- Sales that close after 7 days still come in through the CRM feed, so we see the real cost per sale for every ad.
- If your ad account offers Meta's "qualified leads" goal for website forms, it can learn from CRM sales up to 28 days after the lead. We check that in Ads Manager once we have access.
Why we still send the other stages
- Truth per ad. We store the campaign, ad set and ad ID on every lead. Your CRM can then answer "what did this ad cost per completed consultation and per sale?", which is the number that actually matters.
- Room to grow later. Buyer data could seed buyer audiences down the line. For launch, targeting stays broad, with no custom audiences.
- Reporting inside Meta. Ads Manager can show the consultations and sales it manages to match to an ad next to spend, as report columns. Your CRM stays the source of truth.
Why both the browser and the server
- The browser pixel alone misses events. Ad blockers, Safari and in-app browsers all drop some.
- The server copy fills the gaps and carries better matching data: hashed email and phone, plus the click ID.
- Both copies carry the same event ID, so Meta keeps one and discards the duplicate.
Why we're strict about accuracy and privacy
- Each event says honestly where it happened: website, phone call or other. Meta's terms require it, and mislabelling phone sales as website events can get a dataset restricted.
- No health or service-dog information goes to Meta, ever. That includes the psychiatric and service-dog pages, veteran status, quiz notes and call notes. Meta can restrict a dataset that receives health data, and a restriction would break the sales optimization this whole plan depends on.
- Contact details are hashed before they leave your server. Meta receives scrambled codes, not raw emails or phone numbers. The access token lives in a secret store, never in code or email.
- We never send "negative" events: no refunds, no-shows or lost deals.
3. What Meta will see at each stage
| Stage | What it means | Where it happens | How Meta hears about it | Event ID | What it's used for |
|---|---|---|---|---|---|
| Lead | Quiz submitted | dogacademy.org | Browser + server, same ID | da:lead:{lead_id} | Reporting. Not the ad goal |
| Schedule | The lead's first free consultation is booked | dogacademy.org scheduler (or by an agent on the phone) | Browser + server; agent bookings from your CRM | da:schedule:{lead_id} | Reporting |
| Consultation completed | The call was held with the decision-maker (you define it exactly) | Phone or video | Server, from your CRM or dialer | da:consult:{lead_id} | Cost per consultation |
| Purchase ★ | Trained for Life paid, with the real contract value | Your site, through the pay link. A card taken by phone is reported only | Browser + server from your checkout; phone sales from billing or CRM | da:purchase:{order_id} | The ad goal when paid on your site; cost per sale |
| Sale kept (optional) | 30 days with no refund | Billing | Server | da:salekept:{order_id} | "Kept revenue" reporting only |
How Meta ties each event to a person. Website events carry the ad click cookies (_fbc, _fbp), IP address, browser and hashed contact details. Stages from your CRM reuse the identity your site saved on that lead, plus your lead ID as external_id.
How Meta avoids counting twice. When the browser and server copies share the event name and ID, Meta keeps one. It deduplicates within 48 hours.
The contract value is only used inside Meta's reporting. Ads never show the price.
4. Technical: what gets installed where for your developer
This is the short version. The complete build spec, with the code, database schema, event contract and test plan, is here: Dog Academy Conversions API build instructions. We based it on what we saw on the live site: a Laravel app with Livewire, behind an AWS load balancer, with DNS on Cloudflare. Your developer should correct anything we got wrong.
A. Meta settings
Who: a Dog Academy admin on the Meta Business that owns pixel 481578137302100
- Access. Add Brett as an admin on that Business, the Dog Academy Page and the ad account. Our tools can't see the dataset yet.
- One server sender only. In Events Manager → dataset
481578137302100→ Settings → Manage integrations, confirm nothing else already sends server events: no "Meta-enabled CAPI", Gateway or partner app. - Access token. Business Settings → Users → System users → Add. Give the dataset "Manage pixel" permission and generate a token. Store it in AWS Secrets Manager or SSM as
META_CAPI_ACCESS_TOKEN, never in code, tickets or email. - Staging dataset. Create a second dataset, "DA Staging", with its own token, so test events never mix with real ones.
- Custom conversions (we can do these once we have access):
- "Schedule – in service area";
- "Consultation completed";
- "TFL purchase", once the first real sale has come through.
B. Domain verification
Who: whoever manages Cloudflare
- Add the DNS TXT record Meta gives you for
dogacademy.org(Business Settings → Brand safety → Domains). This covers subdomains too. First check whether another Business already verified the domain.
C. Website: the pixel
Who: your developer, in the Laravel app
- One pixel include in every layout. Create
resources/views/partials/meta-head.blade.phpand include it in the<head>of every Blade layout: standard, booking, program pages (private, group, online), city pages, the scheduler, register and login. Remove every other copy offbq('init')so the pixel never loads twice. - Exclude the health-related funnels. No pixel and no server events on
/pages/*psychiatric*and/pages/*service-dog*. Today/pages/service-dog-training-consultfires the pixel. - Fix the name mix-up. Pass the owner's own details to the pixel's advanced matching: first and last name from the "What's your first and last name?" step, plus email, phone and zip. Pass them only on the signed-in lead's pages, and never the dog's name.
- Replace
/assets/js/tk-fb.jswith the version in the spec. Every event must carry aneventIDmatching the server event. Delete the old name-onlysend-fb-eventcalls soLeadisn't counted twice. - Heyflow city funnels (
training.dogacademy.org): add the same pixel and events in Heyflow's own settings, or keep Meta ads off them. Today they carry no Meta pixel. - Laravel gotchas:
- add
_fbc,_fbpand_fbito theEncryptCookiesexceptions; - set
TrustProxiesso$request->ip()returns the visitor's real IP, not the load balancer's.
- add
D. Website: the server side (Conversions API)
Who: your developer
- Install Meta's Parameter Builder for PHP:
composer require facebook/capi-param-builder-php(version 1.3.2 at the time of writing). It turns the Facebook click ID into the_fbcvalue Meta expects. - Capture the click on every page. Add a
CaptureMetaClickIdsmiddleware to the web group. On any request with?fbclid=it:- sets
_fbcif it's missing (90 days,.dogacademy.org); - keeps
_fbp; - saves the first-touch UTMs plus
campaign_id,adset_idandad_idin the session.
- sets
- Three new tables (migration in the spec):
lead_meta_identities: click IDs, IP, browser and ad IDs per lead;meta_capi_events: an outbox with a uniqueevent_id, so no event is sent twice;lead_stage_events: a log of stage updates received from your CRM or billing.
- Hook the three website events into the existing code:
- Lead in the quiz component's final-submit handler (
frontend.training.book-training). The identity row is saved earlier, at the email step. - Schedule when the lead's first booking is created on
/custom-training-program/schedule-consultation. Reschedules send nothing. - Purchase on the pay-link confirmation page:
da:purchase:{order_id}, with the real value and currency. This is the ad goal. - Event IDs:
da:lead:{lead_id}andda:schedule:{lead_id}, the same in the browser and on the server.
- Lead in the quiz component's final-submit handler (
- Sender. A
CapiServiceplus a queued job (SendMetaEventon ameta-capiqueue) posts each event tohttps://graph.facebook.com/v25.0/481578137302100/events. Rules:- retry with backoff;
- alert on failure;
- send within an hour (Meta rejects anything older than 7 days);
- wrap every Meta call in a try/catch, so tracking can never block a lead or a booking.
- Keep the booking page reachable. Today the scheduler redirects to
/loginwhen the session is lost, which is common in the Facebook and Instagram in-app browsers. Give the post-quiz link a signed URL valid for 48 hours. Otherwise those visitors can't book, and the goal event under-counts. - Pay link and checkout. A signed pay link per order on dogacademy.org. Reuse your course checkout if you can. Reps send it by SMS or email during the call. The confirmation page fires Purchase from the browser, and the server sends the same event.
- Settings in
.env, with secrets injected from the secret store:META_CAPI_ENABLED,META_PIXEL_ID,META_CAPI_ACCESS_TOKEN;META_GRAPH_API_VERSION=v25.0,META_TEST_EVENT_CODE(staging only);META_STAGE_FEED_MODE=offline,META_STAGE_WEBHOOK_SECRET,META_LDU_MODE.
E. The later stages from your systems
Who: your developer builds the endpoint; your ops team connects the CRM, dialer and billing
- Preferred: your CRM, dialer or billing system calls a signed webhook on your own site,
POST https://dogacademy.org/api/meta/stage-events, whenever a lead is booked by an agent, completes a consultation, or buys. Your site forwards the stage to Meta. Nothing posts to Meta directly. - Fallback 1: a daily CSV or Google Sheet export, imported with
php artisan meta:import-stages. It must run at least daily, because Meta refuses events older than 7 days. - Fallback 2: Zapier or Make calls the same webhook. Never use a Zapier "Conversions API" app that posts to Meta directly: that creates a second sender and double counts.
- Each stage needs the Dog Academy lead ID, email and/or phone, the time it happened and, for a purchase, the order ID and contract amount.
- Phone and video stages are labelled honestly as
phone_callorother.
F. Ad links
Who: us, in Ads Manager
- Every ad URL carries this template:
utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}} utm_source=facebookkeeps your phone-number swap working.- Your swap logic should also accept
instagram, because Meta sometimes substitutes it.
G. Never sent to Meta
- Service-dog, psychiatric-service-dog or emotional-support information, including quiz answers and page visits;
- veteran or military status;
- any health information about people;
- pet-insurance answers;
- free-text quiz notes, call notes, recordings;
- card data;
- unhashed contact details;
- refunds, no-shows and lost deals.
- opted-out visitors get no tracking at all (the default);
- they are sent with Meta's "Limited Data Use" flag.
5. How we'll know it's right before any money is spent
Ads launch only when every box below is ticked. Your developer runs the build; we check the numbers in Events Manager with you.
- Each event shows up. On the staging dataset, every event in section 3 appears in Meta's Test Events tool, from the browser and from the server where both send it.
- Nothing counts twice. Browser and server copies of Lead, Schedule and Purchase merge into one, for at least 90% of pairs.
- Clean payloads. Nothing from section 4G. Contact details only as hashes. Event IDs follow the pattern in section 3.
- The ad click is kept.
_fbcis present on at least 90% of leads that came from a Meta ad. - The CRM feed works. A consultation marked held and a sale entered in your CRM reach Meta through the webhook within an hour.
- One full test lead. With a phone number you control, because the quiz rejects fictional numbers: quiz, booking, held, then paid through the pay link (processor test mode, or a small charge you refund). Every step is visible end to end. Your sales team should leave it alone.
- Match quality after 48 hours live. Event Match Quality of at least 7.5 out of 10 for Lead, Schedule and Purchase, aiming for 8. Server coverage of at least 75%, aiming for 90%. Events arrive within an hour.
- The account is ready. Access works, the dataset has a single server sender, the domain is verified, the pixel is on every page it should be and off the service-dog pages, the privacy checks pass, and Ads Manager offers Purchase under the Sales objective.
After launch: a day-7 reconciliation of your CRM counts against what Meta received, then weekly, before any budget increase. We check all of this on our side. We don't write anything into your systems.
6. What we need from you
- Meta access: Brett as admin on the Business that owns pixel
481578137302100, the Page and the ad account. - A developer who can start this week, with this page and the full build spec.
- Your systems: which CRM, dialer and billing tools you use, and whether each can send a webhook. If not, we use the daily file or Zapier.
- Stage definitions:
- what counts as a "completed consultation" (disposition code, minimum talk time, phone or video);
- what counts as a sale (first payment or signed contract, and which amount);
- where refunds are recorded.
- Who books and who runs the free call: mostly the lead on the website, or an agent? A sales advisor or a trainer? The homepage promises a call from a "dog training concierge", while the quiz promises a "free 30-minute virtual meet & greet". Ads should promise the one that is true.
- The pay link (required): your reps send a pay link on the call, so the customer pays on dogacademy.org. Which checkout and processor should it use?
- Numbers:
- lead → booked → completed → sold rates, with monthly counts for the last 3 months (including refunds);
- how long each step takes after the lead, especially the share that happens within 7 days;
- margin per sale;
- the ZIP codes your trainers cover.
- Testing:
- a Dog Academy phone number we can use for the one test lead;
- please delete the partial test lead we left on 28 September: "TEST AdMaker Lead",
admaker-da-test-kd492w@mailinator.com, plus any copies in Klaviyo, Amplitude or the CRM.
- Your counsel's choice on the opt-out mode (section 4G).
7. Timeline and who does what
| When | Dog Academy | Us |
|---|---|---|
| Before day 1 | Meta access for Brett; system-user token and staging dataset; DNS TXT record; answers to section 6 | Check Ads Manager options (drafts only, nothing published) |
| Day 1 | Developer builds all of it: pixel fixes, Conversions API for Lead, Schedule and Purchase, the pay link, the signed booking link, the stage-feed webhook (or the daily file / Zapier). Staging tests, then production behind the on/off switch | Verify every event in Test Events as it lands; set up custom conversions; build the campaign as a draft |
| Day 2 | Connect the CRM, dialer and billing to the webhook; the end-to-end test lead | Check the test lead end to end, including the stage feed |
| Days 3–4 | Nothing, unless a fix is needed | 48-hour quality check (Meta's wait, not ours); go/no-go; launch the first ads, optimized to Purchase |
| Day 7 onward | Keep the stage feed running | Day-7 reconciliation, then weekly; cost per consultation and per sale by ad |
Who does what: your developer builds; your admin handles Meta Business settings, the token and the domain; you and your ops team define the stages and connect the feeds. We handle the checks, the ads and the reporting.
8. Glossary
| Term | Plain meaning |
|---|---|
| Pixel | Meta's code in the visitor's browser that reports page views and actions. |
| Conversions API (CAPI) | Your server reporting the same actions directly to Meta, plus steps that happen off the site. |
| Dataset | The Meta container that receives both copies. Yours is pixel 481578137302100. |
| Event | One reported action, such as Lead, Schedule or Purchase. |
| Event ID | A unique label on each event. Matching IDs let Meta keep one copy when the browser and server both report it. |
| Optimization event | The one event Meta hunts for when it decides who sees the ads. For us: Purchase, the sale paid on your site. |
| Attribution window | How long after an ad click an event still counts for that ad. For website optimization, up to 7 days. |
| Event Match Quality | Meta's 0–10 score for how well an event can be tied to a real person. |
_fbc / _fbp | First-party cookies holding the ad click ID and the browser ID. |
fbclid | The click ID Meta adds to the link when someone taps an ad. |
external_id | Your own lead ID, sent so Meta can join all of one lead's events. |
action_source | Where an event happened: website, phone call or other. |
| Test Events | The Events Manager screen that shows test events live, without counting them in reporting. |
| System user token | The key your server uses to send events. It stays in your secret store. |