{
  "spec_id": "dog-academy-meta-capi-website-route",
  "version": "1.0",
  "created_utc": "2026-09-28",
  "status": "SPEC ONLY. Nothing built, sent or changed in Meta. Every token and secret is a <PLACEHOLDER>. Pixel/dataset ID 481578137302100 is public.",
  "companion_docs": ["DEV-SPEC.md", "QA-RUNBOOK.md", "sources.md"],
  "supersedes": "For the website route this replaces ../../meta-lead-funnel-2026-09-25/capi/event-spec.json, which recommended Instant Forms. The operator locked the destination to DA's own website quiz on 2026-09-28.",

  "decisions": {
    "optimization_event": {
      "event": "Schedule",
      "campaign": "Objective Leads -> conversion location Website -> dataset 481578137302100 -> conversion event Schedule -> attribution 7-day click (whether to add 1-day view is the account-plan thread's call)",
      "marketing_api_equivalent": {
        "campaign.objective": "OUTCOME_LEADS",
        "adset.destination_type": "WEBSITE",
        "adset.optimization_goal": "OFFSITE_CONVERSIONS",
        "adset.promoted_object": { "pixel_id": "481578137302100", "custom_event_type": "SCHEDULE" },
        "adset.attribution_spec": [{ "event_type": "CLICK_THROUGH", "window_days": 7 }]
      },
      "never": "Lead (the form fill) is never the optimization event (operator rule).",
      "upgrade_paths": [
        "Custom conversion 'Schedule - in service area' (website Schedule where in_service_area = yes), once it has volume [H18]",
        "Challenger: 'Maximize number of qualified leads' on Website with CRM stages (crm mode), only if Gate 0-A passes",
        "Genuine on-site downstream events (pay-by-link Purchase on dogacademy.org checkout; ConsultationJoined from a dogacademy.org join page), which require process changes"
      ],
      "sources": ["H3", "H18", "H28"]
    },
    "answer_a_action_source": {
      "per_stage": {
        "Lead": "website",
        "Schedule_self_booked": "website",
        "Schedule_agent_booked": "phone_call",
        "ConsultationCompleted": "phone_call if held by phone; other if video or in person",
        "Purchase_card_on_call_or_virtual_terminal": "phone_call",
        "Purchase_hosted_payment_link_not_on_dogacademy_org": "other",
        "Purchase_paid_on_dogacademy_org_checkout": "website (browser + server, event_id = order ID)",
        "CRM_stage_events_crm_mode_only": "system_generated (Meta requires it for CRM lead-stage events)"
      },
      "can_non_website_events_optimize_a_website_location_campaign": "Treat as NO. Nothing documented allows it. The conversion location is 'the place where your desired business outcome will occur' [H4]. The action source 'is required to determine the campaign objectives for which the event is intended' [D8]. Offline events are for measurement and audiences [D11]. Offline custom events cannot be used for optimization or custom audiences [H12]. The only documented optimization routes for non-website events are Website and in-store (Sales, Purchase, physical_store) [H15] and CRM conversion leads [D13-D16]. The generic line 'All action sources enable ad optimization capabilities' [D1] is not specific to any objective. UNVERIFIED; checked by Gate 0-B.",
      "compliant_alternatives": ["A: website Schedule (recommended)", "B: Schedule custom conversion on a non-sensitive parameter", "C: Website + qualified leads with CRM stages (if Gate 0-A passes)", "D: make downstream steps genuine website events"],
      "rejected": ["E: Website and in-store (DA has no physical store, so physical_store would be inaccurate)", "F: Website and calls (ad-button calls only; not DA's outbound-call outcomes)", "G: relabelling phone/CRM stages as website events tied to the original lead (breaks the action_source accuracy agreement)", "H: Instant Forms (rejected by the operator)"]
    },
    "answer_b_windows": {
      "standard_settings_website": "click-through 1 or 7 days; view-through none or 1 day; engage-through 1 day. They 'inform ad delivery and determine which conversions can be credited' [H7][H8]. 28-day click appears only in comparison reporting [H9].",
      "implication": "Only events within 7 days of the click (1 day of a view) count as optimization events. Lead and Schedule happen minutes after the click, inside the window. A sale 10-20 days after the click never counts toward a Website ad set's optimization.",
      "upload_rules": "event_time must be no more than 7 days old or the whole request fails [D1][D7]. Send within 1 hour: beyond 2 hours performance can drop significantly, and at 24 hours or more there are significant issues [D10]."
    },
    "answer_c_qualified_leads_website_forms": {
      "help_center_says_yes": ["H1: 'can be used with both website forms and instant forms', -9.5% cost per quality lead for website forms", "H2: listed under Leads -> Website, Website and calls, Website and instant forms"],
      "developer_docs_say_no": ["D13: 'currently only compatible with ... Lead Ads (Instant Forms)'", "D14: 'only supports native Lead Ads (Instant Forms)'", "D15: 'Refrain from using this payload specification for other event types, such as website leads'; events that 'do not match a Meta lead ad ... will not be used for training the model'"],
      "faq": "D16 (2026-03-24): 'Instant Forms or Website Forms'; 'Send the lead_id or PII for matching'",
      "status": "UNVERIFIED for DA. Run Gate 0-A (DEV-SPEC section 0.4)."
    }
  },

  "dataset": {
    "id": "481578137302100",
    "role": "The single dataset for browser pixel events, website CAPI events, offline stage events and, in crm mode only, CRM stage events.",
    "staging_dataset": "<STAGING_DATASET_ID> (separate; staging must never post to production because test events are not dropped [D7])",
    "domain": "dogacademy.org (verify the root domain; subdomains are covered) [H24]",
    "single_server_sender": "No Meta-enabled CAPI, Gateway or partner CAPI integration on this dataset alongside the direct integration [H23]"
  },

  "transport": {
    "endpoint": "POST https://graph.facebook.com/{API_VERSION}/481578137302100/events",
    "api_version": "v25.0 (latest, released 2026-02-18) [D27]; configurable via META_GRAPH_API_VERSION",
    "content_type": "application/x-www-form-urlencoded",
    "body_fields": {
      "data": "JSON array of server events (up to 1,000; we send exactly 1 per request)",
      "access_token": "<SYSTEM_USER_TOKEN> in the form body, never in a URL, log or ticket",
      "test_event_code": "<TEST_EVENT_CODE>, staging and the labelled test lead only"
    },
    "success_response": { "events_received": 1, "messages": [], "fbtrace_id": "<FBTRACE_ID>" },
    "error_response_shape": { "error": { "message": "<MESSAGE>", "type": "OAuthException", "code": 190, "error_subcode": "<N>", "fbtrace_id": "<FBTRACE_ID>" } },
    "batch_rule": "If any event in a batch is invalid, the entire batch is rejected [D7], so send 1 event per request.",
    "retry_policy": {
      "retry": "HTTP 0 (transport) or 5xx; Graph codes 1, 2, 4, 17, 32, 341, 613, 80004 (common transient codes; UNVERIFIED list)",
      "alert_and_retry": "190 (token), 200 and 10 (permissions)",
      "dead_no_retry": "100 (invalid parameter), events_received != 1, or event_time older than 7 days minus 1 hour",
      "backoff_seconds": [10, 30, 60, 120, 300, 600, 1800, 3600, 7200],
      "sweeper": "meta:capi:flush every 5 minutes re-dispatches pending or failed rows younger than 6 days 23 hours"
    },
    "freshness_targets": { "web_events_p95": "< 5 minutes", "stage_events_p95": "< 1 hour via webhook; < 24 hours via CSV", "hard_limit": "7 days" }
  },

  "id_conventions": {
    "event_id_format": "da:{event}:{key}, deterministic, UNIQUE in the meta_capi_events outbox, reused on every retry",
    "event_ids": {
      "QuizStart": "da:quizstart:{quiz_session_id}",
      "PartialLead": "da:partiallead:{lead_id}",
      "Lead": "da:lead:{lead_id}",
      "Schedule": "da:schedule:{lead_id}  (first booking per lead; no event on reschedule)",
      "ConsultationCompleted": "da:consult:{lead_id}  (first completion per lead)",
      "Purchase": "da:purchase:{order_id}",
      "SaleKept": "da:salekept:{order_id}",
      "CRM_mode": "da:crm:{stage_key}:{lead_id}"
    },
    "browser_dedup": "fbq('track'|'trackCustom', event_name, params, {eventID: <same event_id>}); same event_name; same custom_data. Meta deduplicates within 48 h and favours the browser event within 5 minutes [D1][D9].",
    "external_id": "raw 'da-lead-{lead_id}'; CAPI sends its SHA-256; the pixel receives the raw string through advanced matching and hashes it itself (UNVERIFIED for this key). Optional second value: 'da-user-{user_id}'."
  },

  "normalization": {
    "hash": "SHA-256, lowercase hex, hashed exactly once [D3][D5]",
    "em": { "rule": "trim, lowercase", "test": { "input": "John_Smith@gmail.com", "normalized": "john_smith@gmail.com", "sha256": "62a14e44f765419d10fea99367361a727c12365e2520f32218d505ed9aa0f62f" } },
    "ph": { "rule": "digits only, drop leading zeros, include country code (US: 1 + 10 digits)", "test": { "input": "(650)555-1212", "normalized": "16505551212", "sha256": "e323ec626319ca94ee8bff2e4c87cf613be6ea19919ed1364124e16807ab3176" } },
    "fn_ln": { "rule": "lowercase, no punctuation, UTF-8; DA has a single name field: fn = first token, ln = last token (omit ln if only one token)" },
    "ct": { "rule": "lowercase, no punctuation, no special characters, no spaces", "example": "San Diego -> sandiego" },
    "st": { "rule": "2-letter ANSI code, lowercase", "test": { "input": "CA", "normalized": "ca", "sha256": "6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126" } },
    "zp": { "rule": "US: first 5 digits", "test": { "input": "94035-1234", "normalized": "94035", "sha256": "1794327a4d443904421f5ca1f3d72d0a62ba2f0770d80755ec2220fc0efb052e" } },
    "country": { "rule": "ISO 3166-1 alpha-2, lowercase, always included", "test": { "input": "US", "normalized": "us", "sha256": "79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621" } },
    "not_hashed": ["client_ip_address", "client_user_agent", "fbc", "fbp"],
    "fbc_format": "fb.{subdomainIndex}.{creationTimeMs}.{fbclid}[.appendix]; server-generated subdomainIndex = 1; use the first time the fbclid was seen; send exactly as returned by the Parameter Builder [D4][D5]",
    "invalid_alone": "Events carrying only ct+country+st+zp+ge+client_user_agent, only db+client_user_agent, only fn+ge or only ln+ge are rejected [D8]"
  },

  "user_data_sets": {
    "web": ["em", "ph", "fn", "ln", "ct", "st", "zp", "country", "external_id", "client_ip_address", "client_user_agent", "fbc", "fbp"],
    "web_quizstart_subset": ["zp", "ct", "st", "country", "client_ip_address", "client_user_agent", "fbc", "fbp"],
    "offline": ["em", "ph", "fn", "ln", "ct", "st", "zp", "country", "external_id", "fbc", "fbp"],
    "crm_mode": ["em", "ph", "fn", "ln", "ct", "st", "zp", "country", "external_id", "fbc"],
    "rule": "Offline and CRM events never carry client_ip_address or client_user_agent. Those must belong to 'the browser corresponding to the event' [D3], and a phone call has none."
  },

  "custom_data_allowlist": {
    "content_name": "book_training_quiz | free_consultation | trained_for_life",
    "content_category": "Lead/Schedule: private | group | online | mixed (quiz step 3). Purchase: in_person | virtual | course",
    "in_service_area": "yes | no (custom parameter; only if DA can compute trainer coverage for the zip)",
    "value": "Purchase and SaleKept only: actual TFL contract amount sold, USD, after discounts, before tax, sent once per order. Internal, never shown in ads.",
    "currency": "USD (Purchase and SaleKept only)",
    "order_id": "Purchase and SaleKept only",
    "event_source": "crm mode only: 'crm'",
    "lead_event_source": "crm mode only: '<DA_CRM_NAME>'"
  },

  "forbidden": {
    "never_send": [
      "Psychiatric-service-dog or service-dog interest, PSD pages, PSD products or scripts",
      "Veteran or military status",
      "Health information about people; pet-insurance or vet-visit answers",
      "Free-text quiz notes (step 16), call notes, recordings, transcripts",
      "Payment card data",
      "Unhashed em/ph/fn/ln/ct/st/zp/country in CAPI"
    ],
    "guard_regex": "psychiatric|service[\\s_-]?dog|\\bpsd\\b|\\besa\\b|emotional[\\s_-]?support|veteran|military|disab|ptsd|insurance|medical|diagnos|therap",
    "guard_applies_to": "event names, custom_data, event_source_url, custom conversion names and rules, UTM values",
    "excluded_paths_no_pixel_no_capi": ["pages/*service-dog*", "pages/*psychiatric*"],
    "negative_events": "Never sent: refund, cancelled, no_show, disqualified, lost",
    "sources": ["H25", "H26", "H27", "D21"]
  },

  "events": [
    {
      "name": "PageView",
      "kind": "standard",
      "priority": "P0",
      "trigger": "Every page load on layouts where the pixel is allowed (all app layouts incl. program pages; not the excluded paths)",
      "senders": ["browser_pixel"],
      "action_source": "website (browser)",
      "role": "audiences, landing-page measurement"
    },
    {
      "name": "QuizStart",
      "kind": "custom",
      "priority": "P2 (optional)",
      "trigger": "Quiz step 1 (zip) accepted by frontend.training.book-training, or the quiz opens at step 2 with ?zip_code=",
      "senders": ["browser_pixel", "server_capi"],
      "action_source": "website",
      "event_id": "da:quizstart:{quiz_session_id}",
      "event_time": "server time when step 1 is accepted",
      "event_source_url": "quiz page URL, path only (from the Livewire request's Referer)",
      "user_data": "web_quizstart_subset",
      "custom_data": { "content_name": "book_training_quiz" },
      "role": "audience of quiz starters (retargeting)",
      "optimization": "never"
    },
    {
      "name": "PartialLead",
      "kind": "custom",
      "priority": "P2 (optional)",
      "trigger": "Email step saves the partial lead (email_owner_submit_step)",
      "senders": ["browser_pixel", "server_capi"],
      "action_source": "website",
      "event_id": "da:partiallead:{lead_id}",
      "user_data": "web",
      "custom_data": { "content_name": "book_training_quiz" },
      "role": "retargeting partial leads",
      "optimization": "never"
    },
    {
      "name": "Lead",
      "kind": "standard",
      "priority": "P0",
      "trigger": "Final quiz submission succeeds (the loading:submit handler marks the lead complete)",
      "senders": ["browser_pixel", "server_capi"],
      "action_source": "website",
      "event_id": "da:lead:{lead_id}",
      "event_time": "lead completed_at (UTC seconds)",
      "event_source_url": "https://dogacademy.org/<QUIZ_PATH> (path only; never the /livewire/message/... endpoint)",
      "user_data": "web",
      "custom_data": { "content_name": "book_training_quiz", "content_category": "<private|group|online|mixed>", "in_service_area": "<yes|no>" },
      "browser_call": "fbq('track', 'Lead', {content_name:'book_training_quiz', content_category:'<CATEGORY>', in_service_area:'<yes|no>'}, {eventID:'da:lead:<LEAD_ID>'})",
      "browser_timing": "Queued in the session and fired on the next page (schedule-consultation), because the submit response redirects",
      "role": "measurement, audiences, the 'lead' for any qualified-leads set-up",
      "optimization": "Eligible under Leads -> Website [H3] but FORBIDDEN (form fill; operator rule)",
      "example_server_event": {
        "event_name": "Lead",
        "event_time": "<UNIX_SECONDS>",
        "event_id": "da:lead:<LEAD_ID>",
        "action_source": "website",
        "event_source_url": "https://dogacademy.org/in-person-dog-training/book-training",
        "user_data": {
          "em": ["<SHA256_EMAIL>"],
          "ph": ["<SHA256_PHONE_WITH_1>"],
          "fn": ["<SHA256_FIRST>"],
          "ln": ["<SHA256_LAST>"],
          "ct": ["<SHA256_CITY>"],
          "st": ["<SHA256_STATE>"],
          "zp": ["<SHA256_ZIP5>"],
          "country": ["79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621"],
          "external_id": ["<SHA256_da-lead-LEAD_ID>"],
          "client_ip_address": "<BROWSER_IP>",
          "client_user_agent": "<BROWSER_UA>",
          "fbc": "<_FBC_COOKIE_VALUE>",
          "fbp": "<_FBP_COOKIE_VALUE>"
        },
        "custom_data": { "content_name": "book_training_quiz", "content_category": "private", "in_service_area": "yes" }
      }
    },
    {
      "name": "Schedule",
      "kind": "standard",
      "priority": "P0 - OPTIMIZATION EVENT",
      "trigger": "The lead's FIRST consultation booking is created on /custom-training-program/schedule-consultation by the lead. No event for reschedules.",
      "senders": ["browser_pixel", "server_capi"],
      "action_source": "website",
      "event_id": "da:schedule:{lead_id}",
      "event_time": "booking created_at (not the appointment time)",
      "event_source_url": "https://dogacademy.org/custom-training-program/schedule-consultation",
      "user_data": "web (IP, UA, fbc and fbp from the booking request)",
      "custom_data": { "content_name": "free_consultation", "content_category": "<private|group|online|mixed>", "in_service_area": "<yes|no>" },
      "browser_call": "fbq('track', 'Schedule', {content_name:'free_consultation', content_category:'<CATEGORY>', in_service_area:'<yes|no>'}, {eventID:'da:schedule:<LEAD_ID>'})",
      "dependencies": ["The scheduler must open without a login wall after the quiz (signed URL), otherwise Schedule under-fires [I2]", "Browser and server custom_data must be identical so the in_service_area custom-conversion rule survives dedup"],
      "role": "optimization event (Leads -> Website), measurement, audiences",
      "optimization": "YES. Listed under Leads -> Website [H3]; happens within minutes of the click, inside the 7-day click window [H7]",
      "example_server_event": {
        "event_name": "Schedule",
        "event_time": "<UNIX_SECONDS_BOOKING_CREATED>",
        "event_id": "da:schedule:<LEAD_ID>",
        "action_source": "website",
        "event_source_url": "https://dogacademy.org/custom-training-program/schedule-consultation",
        "user_data": { "em": ["<SHA256_EMAIL>"], "ph": ["<SHA256_PHONE_WITH_1>"], "external_id": ["<SHA256_da-lead-LEAD_ID>"], "zp": ["<SHA256_ZIP5>"], "country": ["79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621"], "client_ip_address": "<BROWSER_IP>", "client_user_agent": "<BROWSER_UA>", "fbc": "<_FBC>", "fbp": "<_FBP>" },
        "custom_data": { "content_name": "free_consultation", "content_category": "private", "in_service_area": "yes" }
      }
    },
    {
      "name": "Schedule",
      "variant": "agent_booked",
      "kind": "standard",
      "priority": "P1",
      "trigger": "Stage feed: consultation_scheduled for a lead with no website Schedule already recorded",
      "senders": ["server_capi"],
      "action_source": "phone_call",
      "event_id": "da:schedule:{lead_id}",
      "event_time": "booking creation time from the feed (occurred_at)",
      "event_source_url": "omit",
      "user_data": "offline",
      "custom_data": { "content_name": "free_consultation" },
      "role": "measurement",
      "optimization": "Not relied on (non-website action source; UNVERIFIED whether Website ad sets use it)"
    },
    {
      "name": "ConsultationCompleted",
      "kind": "custom (English CamelCase, <= 50 chars)",
      "priority": "P1",
      "trigger": "Stage feed: consultation_completed. Matthew defines the outcome (owner or decision-maker on the call, reached recap or pitch, optional minimum talk time).",
      "senders": ["server_capi"],
      "action_source": "phone_call if channel = phone; other if channel = video or in_person",
      "event_id": "da:consult:{lead_id}",
      "event_time": "call end time (occurred_at)",
      "user_data": "offline",
      "custom_data": { "content_name": "free_consultation" },
      "role": "measurement through the offline custom conversion 'Consultation completed' (create it BEFORE go-live; it only counts data received after creation [H14])",
      "optimization": "No. Offline custom events cannot be used for optimization or custom audiences [H12]. In crm mode, 'Consultation Completed' (CRM) is the qualified-leads target instead.",
      "example_server_event": {
        "event_name": "ConsultationCompleted",
        "event_time": "<UNIX_SECONDS_CALL_END>",
        "event_id": "da:consult:<LEAD_ID>",
        "action_source": "phone_call",
        "user_data": { "em": ["<SHA256_EMAIL>"], "ph": ["<SHA256_PHONE_WITH_1>"], "fn": ["<SHA256_FIRST>"], "ln": ["<SHA256_LAST>"], "zp": ["<SHA256_ZIP5>"], "ct": ["<SHA256_CITY>"], "st": ["<SHA256_STATE>"], "country": ["79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621"], "external_id": ["<SHA256_da-lead-LEAD_ID>"], "fbc": "<STORED_FBC>", "fbp": "<STORED_FBP>" },
        "custom_data": { "content_name": "free_consultation" }
      }
    },
    {
      "name": "Purchase",
      "variant": "offline",
      "kind": "standard",
      "priority": "P1",
      "trigger": "Stage feed: purchase (first successful payment or signed TFL contract) taken by phone, virtual terminal or hosted payment link",
      "senders": ["server_capi"],
      "action_source": "phone_call if payment_channel = phone; other if payment_link or other",
      "event_id": "da:purchase:{order_id}",
      "event_time": "payment capture or contract time (occurred_at)",
      "user_data": "offline",
      "custom_data": { "value": "<ORDER_VALUE_NUMBER>", "currency": "USD", "order_id": "<ORDER_ID>", "content_name": "trained_for_life", "content_category": "<in_person|virtual>" },
      "exclusions": "PSD or service-dog products are never sent",
      "backfill_rule": "If no consultation event exists for the lead, emit ConsultationCompleted first (consultation time, or purchase time minus 1 s)",
      "role": "measurement (value), buyer audiences and exclusions",
      "optimization": "No for Website ad sets: non-website action source, 5.5% of leads, and closes after the 7-day window [H7][H28]",
      "example_server_event": {
        "event_name": "Purchase",
        "event_time": "<UNIX_SECONDS_PAYMENT>",
        "event_id": "da:purchase:<ORDER_ID>",
        "action_source": "phone_call",
        "user_data": { "em": ["<SHA256_EMAIL>"], "ph": ["<SHA256_PHONE_WITH_1>"], "external_id": ["<SHA256_da-lead-LEAD_ID>"], "zp": ["<SHA256_ZIP5>"], "country": ["79adb2a2fce5c6ba215fe5f27f532d4e7edbac4b6a5e09e1ef3a08084a904621"], "fbc": "<STORED_FBC>", "fbp": "<STORED_FBP>" },
        "custom_data": { "value": "<ORDER_VALUE_NUMBER>", "currency": "USD", "order_id": "<ORDER_ID>", "content_name": "trained_for_life", "content_category": "in_person" }
      }
    },
    {
      "name": "Purchase",
      "variant": "site_checkout",
      "kind": "standard",
      "priority": "P2 (only if TFL is paid on dogacademy.org checkout)",
      "trigger": "Order confirmation page on dogacademy.org",
      "senders": ["browser_pixel", "server_capi"],
      "action_source": "website",
      "event_id": "da:purchase:{order_id}",
      "browser_call": "fbq('track', 'Purchase', {value:<VALUE>, currency:'USD', content_name:'trained_for_life', content_category:'<CATEGORY>'}, {eventID:'da:purchase:<ORDER_ID>'})",
      "user_data": "web",
      "note": "The stage feed must then skip this order (payment_channel = website). Course purchases may use content_category = course."
    },
    {
      "name": "SaleKept",
      "kind": "custom",
      "priority": "P3 (optional)",
      "trigger": "Daily job: 30 days after Purchase with no refund recorded (TFL full-refund window)",
      "senders": ["server_capi"],
      "action_source": "system_generated",
      "event_id": "da:salekept:{order_id}",
      "event_time": "purchase time + 30 days (the moment the status became true); send within 7 days of it",
      "user_data": "offline",
      "custom_data": { "value": "<ORDER_VALUE_NUMBER>", "currency": "USD", "order_id": "<ORDER_ID>" },
      "role": "measurement of kept revenue only (offline custom event; not usable for optimization or audiences [H12]); for lookalike seeds use a customer-list audience of kept buyers instead"
    }
  ],

  "crm_mode_events": {
    "enabled_when": "META_STAGE_FEED_MODE=crm, only after Gate 0-A shows 'Maximize number of qualified leads' for Website on dataset 481578137302100",
    "common": {
      "action_source": "system_generated",
      "custom_data": { "event_source": "crm", "lead_event_source": "<DA_CRM_NAME>" },
      "user_data": "crm_mode (no lead_id: website leads have none)",
      "rules": [
        "Send every stage once, the first time it is reached, including the raw lead stage",
        "event_time after the lead creation time (Lead Received = lead time + 1 s)",
        "If a later stage arrives first, backfill the earlier stages 1 s earlier",
        "Names are fixed once the funnel is configured; do not change the dataset afterwards (that restarts training) [I3]",
        "Never add Sale Closed and Purchase together in reporting"
      ]
    },
    "events": [
      { "name": "Lead Received", "event_id": "da:crm:lead_received:{lead_id}", "trigger": "lead completion" },
      { "name": "Consultation Scheduled", "event_id": "da:crm:consultation_scheduled:{lead_id}", "trigger": "first booking (self or agent)" },
      { "name": "Consultation Completed", "event_id": "da:crm:consultation_completed:{lead_id}", "trigger": "consultation held; the proposed target stage", "replaces": "offline ConsultationCompleted" },
      { "name": "Sale Closed", "event_id": "da:crm:sale_closed:{lead_id}", "trigger": "first payment or contract", "custom_data_extra": { "value": "<ORDER_VALUE_NUMBER>", "currency": "USD" } }
    ],
    "documented_gates_for_instant_forms": ["200 leads/month", "lead coverage >= 60%", ">= 7 days of data", "target stage 1-40% of leads within 28 days", "upload at least daily"],
    "sources": ["D13", "D14", "D15", "D16", "H1", "H2", "H5", "I3"]
  },

  "stage_feed": {
    "endpoint": "POST https://dogacademy.org/api/meta/stage-events",
    "auth": {
      "preferred": "HMAC-SHA256: header X-DA-Timestamp (Unix seconds, skew <= 300 s) and X-DA-Signature = 'sha256=' + hex(hmac_sha256(timestamp + '.' + raw_body, <HMAC_SECRET>))",
      "fallback": "Authorization: Bearer <OPTIONAL_BEARER_FOR_NO_CODE_TOOLS> (rotate; no-code tools only)"
    },
    "request_schema": {
      "source": "required: crm | dialer | billing | zapier | make | csv",
      "event_id": "required: unique per source (dedup key)",
      "stage": "required: consultation_scheduled | consultation_completed | purchase | refund | cancelled | no_show | disqualified",
      "occurred_at": "required: ISO-8601 UTC, the real moment of the stage change",
      "lead": { "da_lead_id": "preferred", "email": "fallback match", "phone": "fallback match" },
      "consultation": { "channel": "phone | video | in_person", "duration_seconds": "integer, optional" },
      "order": { "order_id": "required for purchase", "amount": "required for purchase, number", "currency": "USD", "product_line": "tfl_in_person | tfl_virtual | other_training", "payment_channel": "phone | website | payment_link | other" },
      "test": "boolean; routes the event to test_event_code"
    },
    "rejected_fields": "notes, comments, transcripts, recordings, military or veteran status, PSD or health fields: dropped or rejected, never stored",
    "responses": {
      "202": "{\"status\":\"queued\",\"meta_event_id\":\"da:consult:<DA_LEAD_ID>\"}",
      "200_duplicate": "{\"status\":\"duplicate\"}",
      "200_not_sent": "{\"status\":\"stored_not_sent\",\"reason\":\"already_sent|negative_stage|too_old|excluded|unmatched\"}",
      "401": "bad signature or token",
      "422": "validation errors"
    },
    "lead_resolution": "da_lead_id; else the normalized email or phone of the most recent lead created before occurred_at, within 60 days; else unmatched + alert",
    "mapping_offline_mode": {
      "consultation_scheduled": "Schedule / phone_call (result already_sent if the website Schedule exists)",
      "consultation_completed": "ConsultationCompleted / phone_call or other",
      "purchase": "Purchase / phone_call or other (result already_sent if payment_channel = website)",
      "refund|cancelled|no_show|disqualified": "stored only, never sent"
    },
    "too_old": "occurred_at older than 6 d 23 h: stored with result too_old, never sent [D1]",
    "fallbacks": {
      "csv": "php artisan meta:import-stages <file.csv> --source=csv [--dry-run]; columns event_id,stage,occurred_at,da_lead_id,email,phone,channel,order_id,amount,currency,product_line,payment_channel; run at least daily [H13]",
      "google_sheet": "never 'publish to web'; read with a service account or push rows through Zapier/Make to the webhook",
      "zapier_make": "CRM status change or sheet row -> build JSON -> HMAC (Code by Zapier / Make sha256 HMAC) or bearer -> POST to the endpoint; never post to Meta directly"
    }
  },

  "privacy_and_consent": {
    "opt_out_signals": ["Sec-GPC: 1 request header", "'Do not sell or share my personal information' preference (cookie da_dns=1)"],
    "policy_modes": {
      "optout_suppress": "default: no pixel, no cookies, no CAPI for opted-out visitors and leads (outbox status skipped)",
      "optout_ldu": "pixel fbq('dataProcessingOptions', ['LDU'], 0, 0) before init; CAPI data_processing_options ['LDU'], data_processing_options_country 0, data_processing_options_state 0 [D24]"
    },
    "owner": "DA counsel chooses the mode; the build supports both"
  },

  "qa_targets": {
    "emq_internal_targets": { "Lead": ">= 8.0 (launch minimum 7.5)", "Schedule": ">= 8.0 (launch minimum 7.5)", "PartialLead": ">= 7.0", "QuizStart": "informational", "note": "Meta publishes no pass mark; EMQ exists only for website events [H19][D8]" },
    "offline_match_key_coverage": { "em": ">= 99%", "ph": ">= 95%", "external_id": "100%", "fbc_on_meta_click_leads": ">= 90%" },
    "event_coverage": "Meta aims for >= 75% CAPI vs pixel [H20]; our target >= 90% for Lead and Schedule, with server events >= browser events",
    "dedup": "Dedup-key overlap >= 90% for Lead and Schedule; the browser and server twin both show in Test Events and one is deduplicated",
    "stage_coverage": ">= 98% of eligible stage changes sent within 24 h; 100% within 6 days",
    "reconciliation_7_day": "app truth vs sent: gap fully explained by exclusions; sent vs acknowledged 100%; acknowledged vs Meta /stats SERVER_ONLY within +/-2%"
  },

  "unverified": [
    "U1 Whether phone_call, other or system_generated events, or custom conversions on them, can be the optimization event of a Website ad set (docs imply no)",
    "U2 Whether 'Maximize number of qualified leads' works for website forms in DA's account; its signal source and window",
    "U3 Whether the pixel hashes external_id automatically",
    "U4 Whether Meta merges server user_data into a deduplicated browser event",
    "U5 Whether the 28-day-click comparison includes phone_call events",
    "U6 The exact retryable Graph error-code list",
    "U7 Whether the current domain-verification tag belongs to DA's Business",
    "U8 Whether dataset 481578137302100 is consolidated for offline events (is_consolidated_container)",
    "U9 Whether Livewire v2 fires browser events before a redirect in the same response (the session queue avoids depending on it)"
  ]
}
