Dog Academy: Meta Conversions API, developer handoff
For Dog Academy's developer, 29 September 2026. Prepared by AdMaker for Brett and Matt. Nothing has been installed or changed yet.
You decide how to build it. This document says what to build, where we'd put each piece in your app, and how we'll know it works. We wrote it from what the live site shows from outside: a Laravel app with Livewire, behind an AWS load balancer, with DNS on Cloudflare. Correct anything we got wrong.
Reference code for every piece is in the full build spec: the migration, service, job, middleware and
tests. Event rules in machine-readable form are in event-contract.json.
1. What you're building
- Three website events, sent twice. Lead (the quiz is submitted), Schedule (the first free consultation is
booked) and Purchase (Trained for Life paid on dogacademy.org). Both the browser pixel and your server send each
one to Meta dataset
481578137302100, with the same event ID, so Meta keeps one copy. - Later stages from your own systems. Your CRM, dialer or billing tells your site when a consultation is held or a sale closes. It does this through one small signed endpoint. Your site forwards the stage to Meta. Nothing else talks to Meta.
- Ads optimize to the sale: Purchase. Meta can only optimize to a sale that happens on your website, within 7 days of the ad click. So the rep closes on the call and sends a pay link, and the customer pays on dogacademy.org. That checkout fires Purchase. Sales paid by card over the phone still come in through the stage feed, but only for reporting.
Three hard rules:
- Tracking must never block the quiz or the booking. Every Meta call is queued and wrapped in try/catch.
- Ship it behind
META_CAPI_ENABLED, off by default, and turn it on per environment after the tests. - Never send health or service-dog information (section 5).
2. How it works
- Ad click. The visitor arrives with
fbclidin the URL. Middleware turns it into the_fbccookie and keeps_fbp. - Email step. When the quiz saves the lead, your app stores the click IDs, IP, browser and first-touch ad IDs against that lead.
- Quiz submitted. The browser fires
LeadwitheventID = da:lead:{lead_id}. The server queues the same event with the same ID. - Consultation booked. The same pattern, with
Scheduleandda:schedule:{lead_id}. First booking only. - Paid on the site. The rep sends the pay link on the call. When the customer pays, the confirmation page fires
PurchasewitheventID = da:purchase:{order_id}, plus the value and currency. The server sends the same event, matched to the lead's saved identity. This is the event the ads optimize to. - Later stages. The CRM, dialer or billing calls
POST /api/meta/stage-events. Your app looks up the lead's saved identity and sendsConsultationCompletedor a phone-paidPurchaseto Meta, labelled honestly as a phone call or other.
3. What to build, and where we'd put it
| # | What | Where (our suggestion) | Done when |
|---|---|---|---|
| 1 | Meta access, system-user token, staging dataset | Meta Business Settings, done by a DA admin. Give the system user "Manage pixel" on dataset 481578137302100. Create a second dataset, "DA Staging", with its own token. | Brett can see the dataset. Both tokens are stored in your secret store, never in code, tickets or email. |
| 2 | Domain verification | A Cloudflare DNS TXT record for dogacademy.org, taken from Business Settings → Brand safety → Domains | Meta shows the domain as Verified. |
| 3 | One server sender | Events Manager → dataset → Settings → Manage integrations | Nothing else sends server events: no "Meta-enabled CAPI", no Gateway, no partner app. |
| 4 | Config | .env + config/meta.php: META_CAPI_ENABLED, META_PIXEL_ID=481578137302100, META_CAPI_ACCESS_TOKEN, META_GRAPH_API_VERSION=v25.0, META_TEST_EVENT_CODE (staging only), META_STAGE_WEBHOOK_SECRET, META_LDU_MODE. Secrets are injected from AWS Secrets Manager or SSM. | The token never appears in the repo or in logs. |
| 5 | Pixel on every page | One partial, resources/views/partials/meta-head.blade.php, included in the <head> of every Blade layout: standard, booking, program pages (private, group, online), city pages, the scheduler, register and login | Each page loads the pixel exactly once, with one fbq('init'). |
| 6 | No pixel on health funnels | Layouts or routes for /pages/*psychiatric* and /pages/*service-dog* | No pixel and no server events on those pages. Today /pages/service-dog-training-consult fires the pixel. |
| 7 | Browser events with event IDs | Replace public/assets/js/tk-fb.js (v2 is in the full spec §5.4). Livewire dispatches send-fb-event with {name, params, eventId}. | Every non-PageView event carries an eventID that matches the server event. The old name-only dispatches are deleted, so Lead isn't counted twice. |
| 8 | Correct matching | The pixel's advanced matching, on the lead's pages | The owner's own name, email, phone and ZIP are passed. The dog's-name field is never read as the owner's name. |
| 9 | Click capture | app/Http/Middleware/CaptureMetaClickIds.php in the web group, using composer require facebook/capi-param-builder-php (1.3.2) | With ?fbclid=, _fbc is set if missing (90 days, .dogacademy.org) and _fbp is kept. The first-touch UTMs plus campaign_id, adset_id and ad_id are saved in the session. |
| 10 | Laravel gotchas | EncryptCookies::$except gets _fbc, _fbp and _fbi. TrustProxies is set for the AWS load balancer. | $request->ip() returns the visitor's IP, not the load balancer's. |
| 11 | Three tables | One migration (full spec §3.5): lead_meta_identities (click IDs, IP, browser, ad IDs per lead), meta_capi_events (outbox, unique event_id), lead_stage_events (the inbound stage log) | The migration runs on staging. Call notes, recordings and card data are never stored in these tables or in logs. |
| 12 | Server sender | app/Services/Meta/CapiService.php + app/Jobs/SendMetaEvent.php on a meta-capi queue. POST https://graph.facebook.com/v25.0/481578137302100/events, form-encoded, one event per request. | Retries with backoff. Failures alert. Nothing older than 7 days is sent. Most events arrive within an hour. A Meta outage never affects the quiz. |
| 13 | Lead hook | The quiz component frontend.training.book-training: save the identity row at the email step and fire Lead in the final-submit (loading:submit) handler | One Lead per lead, da:lead:{lead_id}, from browser and server. |
| 14 | Schedule hook | Where the lead's first booking is created on /custom-training-program/schedule-consultation | One Schedule per lead, da:schedule:{lead_id}, for reporting. Reschedules send nothing. |
| 15 | Pay link + Purchase (the optimization event) | A signed pay-link route per order on dogacademy.org (for example /pay/{order}), using your existing checkout and processor. Reuse the course checkout if you can. The order confirmation page fires the event. | A paid order fires Purchase from the browser and the server: da:purchase:{order_id}, the real value, currency: USD, matched to the lead. Reps can send the link by SMS or email during the call. |
| 16 | Booking link that survives in-app browsers | The post-quiz redirect: a signed URL valid for 48 hours, instead of a session that bounces to /login | A lead opening the booking page inside the Facebook or Instagram app can book without logging in. |
| 17 | Stage endpoint | routes/api.php: POST /api/meta/stage-events + VerifyStageSignature (HMAC-SHA256 over timestamp.body, headers X-DA-Timestamp and X-DA-Signature) + a StageEventIngestor (full spec §6) | A signed test call returns 202 queued. A replay returns duplicate. Negative stages are stored, not sent. |
| 18 | Connect your systems | Preferred: your CRM, dialer and billing call the endpoint on status changes. Fallbacks: a daily CSV or Sheet import (php artisan meta:import-stages), or Zapier or Make posting to the same endpoint. | A consultation marked held and a test sale show up in Meta within an hour. No tool posts to Meta directly. |
| 19 | Heyflow city funnels | Heyflow's own settings (training.dogacademy.org) | Your call: add the same pixel and events, or keep Meta ads off them. |
| 20 | Privacy mode | META_LDU_MODE + Global Privacy Control handling (full spec §7.5) | Counsel picks a mode: either opted-out visitors get no tracking, or they are sent with Meta's Limited Data Use flag. |
4. Event contract
| Event | Fires when | Sent by | action_source |
event_id |
custom_data |
|---|---|---|---|---|---|
| Lead | Final quiz submit succeeds | Browser + server | website | da:lead:{lead_id} | content_name: book_training_quiz, content_category, in_service_area |
| Schedule | The lead's first booking is created on the site | Browser + server | website | da:schedule:{lead_id} | content_name: free_consultation, content_category, in_service_area |
| Schedule (agent) | Stage feed consultation_scheduled, if there is no website Schedule | Server | phone_call | da:schedule:{lead_id} | the same |
| ConsultationCompleted | Stage feed consultation_completed (first per lead) | Server | phone_call; other for video | da:consult:{lead_id} | none |
| Purchase ★ | The customer pays through the pay link on dogacademy.org. Phone-card sales arrive through the stage feed instead. | Browser + server (site checkout); server only for phone sales | website (site checkout); phone_call or other (phone) | da:purchase:{order_id} | value, currency: USD, order_id, content_name: trained_for_life |
| SaleKept (optional) | A daily job, 30 days after a purchase with no refund | Server | system_generated | da:salekept:{order_id} | value, currency, order_id |
★ Purchase paid on dogacademy.org is the event the ads optimize to (Sales objective → Website → Purchase). Lead and Schedule are for reporting and diagnosis.
User data:
- Website events: hashed
em,ph,fn,ln,ct,st,zp,countryandexternal_id, plus rawclient_ip_address,client_user_agent,fbcandfbp. - Stage-feed events: the same hashed keys plus
fbc/fbpsaved on the lead. They carry no IP or browser, because a phone call has none.
Hashing: SHA-256, lowercase hex, exactly once.
- Email: trimmed and lowercased.
- Phone: digits with the country code (
16505551212). - ZIP: the first 5 digits.
- City: lowercase, no spaces.
- State: 2 letters.
- Country:
us. external_id:da-lead-{lead_id}.
The value sent with a purchase is used only inside Meta reporting. Ads never show a price.
5. Never sent to Meta
- Service-dog, psychiatric-service-dog or emotional-support information: quiz answers, page visits, products.
- Veteran or military status.
- Any health information, and pet-insurance or vet-visit answers.
- Free-text quiz notes, call notes, recordings.
- Card data.
- Unhashed contact details.
- Negative outcomes: refunds, no-shows, cancellations and lost deals are stored, but never sent.
6. Examples (placeholders in angle brackets)
Browser:
fbq('track', 'Schedule', {content_name: 'free_consultation'}, {eventID: 'da:schedule:<LEAD_ID>'});
// on the pay-link confirmation page (the optimization event):
fbq('track', 'Purchase', {value: <AMOUNT>, currency: 'USD', content_name: 'trained_for_life'}, {eventID: 'da:purchase:<ORDER_ID>'});
Server, the same event (the data array in the form body; access_token in the form body, never in the URL):
[{
"event_name": "Schedule",
"event_time": 1759167900,
"event_id": "da:schedule:<LEAD_ID>",
"action_source": "website",
"event_source_url": "https://dogacademy.org/custom-training-program/schedule-consultation",
"user_data": {
"em": ["<SHA256>"], "ph": ["<SHA256>"], "fn": ["<SHA256>"], "ln": ["<SHA256>"],
"ct": ["<SHA256>"], "st": ["<SHA256>"], "zp": ["<SHA256>"], "country": ["<SHA256>"],
"external_id": ["<SHA256 of da-lead-LEAD_ID>"],
"client_ip_address": "<VISITOR_IP>", "client_user_agent": "<VISITOR_UA>",
"fbc": "<_fbc cookie>", "fbp": "<_fbp cookie>"
},
"custom_data": {"content_name": "free_consultation", "in_service_area": "yes"}
}]
Stage feed: your CRM, dialer or billing calls your site, signed with X-DA-Timestamp and
X-DA-Signature: sha256=<hmac>.
{
"source": "dialer",
"event_id": "<SOURCE_UNIQUE_EVENT_ID>",
"stage": "purchase",
"occurred_at": "2026-10-02T17:31:05Z",
"lead": {"da_lead_id": "<LEAD_ID>"},
"order": {"order_id": "<ORDER_ID>", "amount": <AMOUNT>, "currency": "USD",
"product_line": "tfl_in_person", "payment_channel": "phone"},
"test": false
}
Field rules:
stageis one of:consultation_scheduled,consultation_completed,purchase,refund,cancelled,no_show,disqualified.occurred_atis the real moment of the change, not the upload time.
Responses:
202 queued200 duplicate200 stored_not_sent(already sent, negative stage, too old, excluded, or unmatched)401for a bad signature422for invalid input
7. How we'll know it works (all must pass before ads spend)
- ☐ Staging: every event appears in Events Manager → Test Events, and the browser and server copies of Lead and Schedule merge (90%+ deduplicated).
- ☐ Payloads: they contain nothing from section 5. Contact details are hashed only. IDs follow the patterns in section 4.
- ☐ Click ID:
_fbcis present on at least 90% of leads that came from a Meta ad. - ☐ Pay link: a test payment (processor test mode on staging) fires
Purchasefrom the browser and the server, deduplicated, with the right value, and matched to the test lead. - ☐ Stage feed: a consultation marked held and a phone-paid test sale reach Meta within an hour. A replay is rejected as a duplicate.
- ☐ Resilience: turning off network access to Meta does not break the quiz or the booking page.
- ☐ One end-to-end test lead, on a Dog Academy phone number, because the quiz rejects fictional numbers: quiz, booking, held, then paid through the pay link. Each step is visible in Events Manager.
- ☐ Ads Manager offers Purchase on dataset
481578137302100under Sales → Website. - ☐ 48 hours after go-live: Event Match Quality of at least 7.5 on Lead and Schedule (target 8), and events arriving within an hour.
We check all of this with you in Events Manager. We never write into your systems.
8. Decisions for your team
- Your CRM, dialer and billing tools: can each call a webhook? If not, use the daily file or Zapier.
- What counts as "consultation completed" (disposition, minimum talk time, phone or video), and what counts as a sale (first payment or signed contract, and which amount).
- The pay link: which checkout and processor it uses (reuse the course checkout if you can), and whether reps send it by SMS, email or both. This is required, because it's what lets the ads optimize to sales.
- Heyflow city funnels: pixel them, or keep them off Meta.
- Privacy mode: counsel's choice (item 20).
Questions go to Brett. Reference code: the full build spec. The business overview for Matt is here.