Dog Academy × Meta: tracking the leads that turn into sales

For Matt · Dog Academy · 28 September 2026 · Status: plan only, nothing has been installed or changed yet

Download as PDF

For your developer: the one-page minimum to optimize ads to sales is here. This page is the long version.

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:

What we're proposing

  1. 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.
  2. Add the Conversions API. Your server sends the same events as the browser, with a shared ID so nothing counts twice.
  3. Track three website events properly: Lead (quiz submitted), Schedule (free consultation booked) and Purchase (Trained for Life paid on your site).
  4. 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.
  5. 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.
  6. Test everything with Meta's test tools and one real end-to-end test lead. Then we launch ads optimized to Purchase, the sale.
The one-line version: the ads will be told to find people who buy. For Meta to see the sale, Trained for Life is paid on dogacademy.org through a pay link the rep sends on the call. Everything else is reported back, so we see the real cost per consultation and per sale for each ad.

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 ads optimize to the sale Ad clickVisitor taps the ad Quiz submittedLead event, sent from your site Free consultation bookedSchedule event, sent from your site Consultation heldOn the phone; your CRM reports it Trained for Life paid on dogacademy.orgPurchase event, sent from your siteOptimize here A card taken over the phone can be reported to Meta, but only a payment on your site can train the ads. The payment has to land within 7 days of the ad click.
The sale is the ad goal. For Meta to learn from it, the customer pays on dogacademy.org through the pay link the rep sends on the call.

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

Why we still send the other stages

Why both the browser and the server

Why we're strict about accuracy and privacy

3. What Meta will see at each stage

StageWhat it meansWhere it happensHow Meta hears about itEvent IDWhat it's used for
LeadQuiz submitteddogacademy.orgBrowser + server, same IDda:lead:{lead_id}Reporting. Not the ad goal
ScheduleThe lead's first free consultation is bookeddogacademy.org scheduler (or by an agent on the phone)Browser + server; agent bookings from your CRMda:schedule:{lead_id}Reporting
Consultation completedThe call was held with the decision-maker (you define it exactly)Phone or videoServer, from your CRM or dialerda:consult:{lead_id}Cost per consultation
Purchase ★Trained for Life paid, with the real contract valueYour site, through the pay link. A card taken by phone is reported onlyBrowser + server from your checkout; phone sales from billing or CRMda:purchase:{order_id}The ad goal when paid on your site; cost per sale
Sale kept (optional)30 days with no refundBillingServerda: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.

Two senders, one dataset Browser and server send the same events; Meta counts each once The visitor’s browser Pixel on every pageSends Lead, Schedule,Purchase, each with an IDKeeps _fbc and _fbp CRM, dialer, billing Calls your site when aconsult is held or asale closes (lead ID) Laravel app (server) Saves the click ID andhashed contact per leadSends the same eventsthrough CAPI webhook (lead ID) same event via CAPI pixel event+ event ID Meta dataset 481578137302100 Matches the two copies by event ID, counts onceOptimizes to Purchase, paid on your siteYour CRM never talks to Meta directly
The browser and your server report the same website events with the same ID. Your CRM tells your site about later stages, and the site sends them on.

A. Meta settings

Who: a Dog Academy admin on the Meta Business that owns pixel 481578137302100

  1. 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.
  2. 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.
  3. 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.
  4. Staging dataset. Create a second dataset, "DA Staging", with its own token, so test events never mix with real ones.
  5. 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

  1. One pixel include in every layout. Create resources/views/partials/meta-head.blade.php and 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 of fbq('init') so the pixel never loads twice.
  2. Exclude the health-related funnels. No pixel and no server events on /pages/*psychiatric* and /pages/*service-dog*. Today /pages/service-dog-training-consult fires the pixel.
  3. 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.
  4. Replace /assets/js/tk-fb.js with the version in the spec. Every event must carry an eventID matching the server event. Delete the old name-only send-fb-event calls so Lead isn't counted twice.
  5. 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.
  6. Laravel gotchas:
    • add _fbc, _fbp and _fbi to the EncryptCookies exceptions;
    • set TrustProxies so $request->ip() returns the visitor's real IP, not the load balancer's.

D. Website: the server side (Conversions API)

Who: your developer

  1. 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 _fbc value Meta expects.
  2. Capture the click on every page. Add a CaptureMetaClickIds middleware to the web group. On any request with ?fbclid= it:
    • sets _fbc if it's missing (90 days, .dogacademy.org);
    • keeps _fbp;
    • saves the first-touch UTMs plus campaign_id, adset_id and ad_id in the session.
  3. 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 unique event_id, so no event is sent twice;
    • lead_stage_events: a log of stage updates received from your CRM or billing.
  4. 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} and da:schedule:{lead_id}, the same in the browser and on the server.
  5. Sender. A CapiService plus a queued job (SendMetaEvent on a meta-capi queue) posts each event to https://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.
  6. Keep the booking page reachable. Today the scheduler redirects to /login when 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.
  7. 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.
  8. 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.
    Ship it switched off, then turn it on per environment after the tests.

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_call or other.

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=facebook keeps 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.
Privacy choice for your counsel: there is no cookie-consent tool on the site today. The spec builds an opt-out that honours the browser's "Global Privacy Control" signal and a "Do not sell or share" choice. Counsel picks one of two modes:
  • 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.

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

7. Timeline and who does what

WhenDog AcademyUs
Before day 1Meta access for Brett; system-user token and staging dataset; DNS TXT record; answers to section 6Check Ads Manager options (drafts only, nothing published)
Day 1Developer 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 switchVerify every event in Test Events as it lands; set up custom conversions; build the campaign as a draft
Day 2Connect the CRM, dialer and billing to the webhook; the end-to-end test leadCheck the test lead end to end, including the stage feed
Days 3–4Nothing, unless a fix is needed48-hour quality check (Meta's wait, not ours); go/no-go; launch the first ads, optimized to Purchase
Day 7 onwardKeep the stage feed runningDay-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

TermPlain meaning
PixelMeta'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.
DatasetThe Meta container that receives both copies. Yours is pixel 481578137302100.
EventOne reported action, such as Lead, Schedule or Purchase.
Event IDA unique label on each event. Matching IDs let Meta keep one copy when the browser and server both report it.
Optimization eventThe one event Meta hunts for when it decides who sees the ads. For us: Purchase, the sale paid on your site.
Attribution windowHow long after an ad click an event still counts for that ad. For website optimization, up to 7 days.
Event Match QualityMeta's 0–10 score for how well an event can be tied to a real person.
_fbc / _fbpFirst-party cookies holding the ad click ID and the browser ID.
fbclidThe click ID Meta adds to the link when someone taps an ad.
external_idYour own lead ID, sent so Meta can join all of one lead's events.
action_sourceWhere an event happened: website, phone call or other.
Test EventsThe Events Manager screen that shows test events live, without counting them in reporting.
System user tokenThe key your server uses to send events. It stays in your secret store.