=== CAPI Suite: Meta Pixel, Pinterest & TikTok for WooCommerce === Contributors: suhanduman Tags: facebook pixel, meta pixel, tiktok pixel, pinterest tag, conversions api Requires at least: 6.0 Tested up to: 7.1 Stable tag: 3.11.0 Requires PHP: 7.4 WC requires at least: 8.0 WC tested up to: 11.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Meta Pixel, Pinterest & TikTok server-side CAPI. Recovers conversions that ad blockers and iOS hide. No monthly server, no pro tier, no limits. == Our Philosophy == This plugin is free. Not "free with limits" — just free. Every feature works, there is no pro version waiting behind a paywall, and the code is GPL, so you can read every line that touches your customers' data. == Description == **Meta Pixel, Pinterest and TikTok, sent server-side, in one free plugin.** A bundled Google Tag Manager template covers browser-side tracking. Sixteen events you switch on individually — PageView, ViewContent, ViewCategory, SelectItem, AddToCart, ViewCart, InitiateCheckout, AddShippingInfo, AddPaymentInfo, Purchase, Search, CompleteRegistration, AddToWishlist, Lead, Login and Comment — plus subscription renewals handled on their own terms. Classic and block-based checkout, HPOS compatible, each platform retried independently on failure. No cloud server to rent, no premium tier to unlock, no event limit. **Your match quality climbs on its own after you update.** Twenty identity parameters are assembled per event and normalised exactly the way Meta requires before they are hashed. The hashed keys are then remembered for up to 180 days behind a consent gate, so a returning visitor arrives already identified instead of anonymous. Nothing raw is ever written to disk: only salted SHA-256 hashes, which survive even a WordPress security-key rotation. **Attribution survives the whole journey, not just the click.** Eight click identifiers are captured and kept — `fbclid`, `gclid`, `wbraid`, `gbraid`, `ttclid`, `pina_id`, `_epik` and `_ttp` — written into first-party cookies client-side so a cached landing page never loses them, then persisted onto the order so a payment that completes days later is still credited to the campaign that earned it. Subscription renewals inherit the original order's identifiers instead of being credited to a fresh ad. **Bots don't get counted as customers — and real shoppers on a VPN still are.** Datacenter ranges are weighed against actual browser behaviour: mouse movement, scrolling, and whether a cart ever appears. A doubtful visit is held rather than thrown away, and if that visitor goes on to act like a real person, the whole funnel is released and reported retroactively. Purchases are never held back while any of this is decided. Apple iCloud Private Relay and corporate VPN egress ranges are recognised as real people, not as datacenters. **Safari visitors stay attributable for 90 days, not 7.** The cookie is written by the server, so Safari's seven-day cap on script-written cookies stops resetting the visitor every week. **Your orders are counted once, not twice.** The same event ID goes out browser-side and server-side within Meta's 48-hour deduplication window, so each platform matches them up instead of double-counting. A browser event that falls outside the window is suppressed rather than sent as a second conversion. **You keep your analytics when a visitor declines cookies.** The event still goes through for counting, with personal details stripped out first. Five consent platforms are recognised automatically — CookieYes, Cookiebot, Complianz, iubenda and Termly — and a filter covers anything else, including OneTrust, Usercentrics and hand-rolled banners. = Built like infrastructure, not like a tracking snippet = * **Nothing is lost when a platform has a bad minute.** Events queue and retry per platform: if Meta accepts an event and Pinterest times out, only Pinterest is retried. Dispatch runs on Action Scheduler rather than WP-Cron, so a stalled queue is visible and recoverable instead of silent. * **Built for shared hosting.** Counters are incremented atomically in SQL rather than read-modify-written, so overlapping requests cannot lose each other's work. The bot blocklist is stored as roughly 10,000 pre-computed numeric ranges queried through a database index, not as a text list parsed on every request. * **The REST endpoint is not an open door.** Events are signed with a rotating HMAC token, nonce-verified where a form is involved, and rate-limited per IP — with the limiter backed by a database table so it still works on hosts with no object cache. * **A bundled GTM template gets your browser-side tags running in minutes.** 37 tags, 12 triggers and 17 variables covering GA4, Meta Pixel, TikTok Pixel, Pinterest Tag and Google Ads, with Conversion Linker already attached — normally an afternoon of manual GTM work. A written manual is included for anyone who would rather build it by hand. * **You can see exactly what was sent, and undo a bad exclude.** The Event Log shows the identity parameters that went out with each event and the platform's own response. Its By-IP view groups repeat traffic together, flags real buyers so you don't accidentally exclude them, and excludes a confirmed bot in one click. * **Page caching stays on.** Works with LiteSpeed, WP Rocket, Varnish and Cloudflare full-page cache. * **Queue health is visible from your WordPress dashboard.** A widget shows backlog size, the oldest pending event, and when the last dispatch succeeded, so a stuck queue doesn't go unnoticed. * **The bot blocklist maintains itself.** Ranges for nine cloud providers ship with the plugin and refresh daily in the background. You can add your own, exclude any rule you disagree with, and download the active list as plain text. * **Test before you trust it.** Send a test event to Meta or TikTok from the settings page and watch it arrive in Events Manager, with test-event codes handled for you. * **Privacy work is already done.** The plugin registers with WordPress's personal-data export and erasure tools, contributes its own section to the privacy policy WordPress generates for you, and stores IP addresses only as salted hashes. = What this plugin stores = To match server-side conversion events to the right person, the plugin stores **SHA-256 hashes** of contact details a visitor provides — email, phone, name and address components — against an opaque visitor identifier. **Raw contact details are never written to disk**; values are hashed before storage and the hash cannot be reversed. * **Two first-party cookies.** An opaque visitor identifier (1 year) and a Meta-format browser identifier `_fbp` (90 days). * **180-day retention.** Records are deleted 180 days after they were last seen. Filterable via `mcapi_identity_retention_days`. * **Consent-gated.** Nothing is stored, and nothing already stored is read, where marketing consent has been refused. Withdrawing consent expires both cookies and deletes the stored record. * **Privacy tools.** WordPress's personal-data export and erasure both cover this data. * **Shared devices.** A stored record is replaced whenever a new person identifies themselves and is deleted at logout. A second person who only browses without identifying themselves is covered by the 180-day expiry rather than by an immediate reset — worth knowing if your customers use shared or public computers. == Frequently Asked Questions == = Does this plugin replace the Meta Pixel? = No, it works alongside it. The plugin sends server-side (CAPI) events, while GTM handles the browser-side Pixel. Both use the same `event_id`, so Meta merges them automatically without counting anything twice. = What is the difference between this and a GTM Server Container? = A GTM Server Container runs on Google Cloud and costs money every month. This plugin does the same job directly from your WordPress server — no extra infrastructure, no extra bill. = Does it work with page caching plugins (WP Rocket, LiteSpeed, etc.)? = Yes. PageView and ViewCategory events fire from JavaScript, so they work even on fully cached pages. Cart, checkout, and purchase pages are not cached by default. = What plugins are required? = WooCommerce. That's it. If another plugin on your site also manages Google Tag Manager or sends e-commerce events, turn its e-commerce tracking off so the same event is not counted twice. The Health tab tells you if it finds a pixel or measurement ID configured in two places at once. = Is there a pro version? = No. Everything is included. = My events aren't showing in Meta Events Manager. = Open the **Event Log** tab. If events appear there with "Success (Meta)", the plugin is sending — anything missing on Meta's end is a Pixel ID / Access Token mismatch. If the log is empty, your JS optimizer is likely deferring the inline scripts (see next answer) or your CMP auto-blocker converted them to `type="text/plain"` (see the CMP question below). = JS optimizer (LiteSpeed / WP Rocket / Autoptimize) — what do I configure? = Add these four IDs to your optimizer's "exclude from defer / combine" list: `mcapi-pageview-init`, `mcapi-viewcontent-events`, `mcapi-viewcategory-events`, `mcapi-frontend-events`. Cloudflare Rocket Loader is handled automatically via `data-cfasync="false"`. = Does it work with a block-based theme (Twenty Twenty-Five etc.)? = Yes. = GTM Preview shows my browser tags firing, but the plugin's Event Log is empty. = Your CMP's auto-blocker is converting the plugin's inline scripts to `type="text/plain"`. The plugin already carries opt-out attributes for Cookiebot, CookieYes, and Complianz; less common CMPs (OneTrust etc.) need the `mcapi_inline_script_attrs` filter — see **CMP Auto-Blocking** in Advanced Configuration. = I sell subscriptions — Meta is over-attributing renewals to old ads. = The plugin auto-detects WooCommerce Subscriptions and offers three behavior modes (Default / Skip / Subscribe + SubscriptionRenewal). Pick Skip or the dedicated-events mode to keep `Purchase` clean. See **WooCommerce Subscriptions** in Advanced Configuration. = EU traffic — does the plugin respect cookie-banner consent for CAPI? = Not by default — server-side CAPI fires from PHP, doesn't see your `gtag('consent', ...)` signals. The Privacy & Consent section has a **Strict server-side consent mode** toggle: when consent is denied, hashed PII is stripped from the CAPI payload but the event still ships with its `event_id`, so Meta's browser↔CAPI dedup keeps working without identifying data. Recommended ON for EU stores. See **Strict server-side consent mode** in Advanced Configuration. = Will the datacenter IP filter block my real VPN customers? = Rarely. Visitors with click IDs (fbclid / gclid / ttclid), Apple Private Relay IPs, logged-in customers, or prior-visit `_fbp` / `_ga` cookies all bypass the filter. Purchase events are never blocked. A brand-new VPN visitor with no cookies has their first PageView held; if they purchase, the full funnel is replayed so Meta sees the complete journey. Every blocked request is auditable in the **Excluded Traffic** tab. = How do I confirm TikTok is actually matching my customers' phone numbers? = TikTok's Events Manager can show you something this plugin cannot see from your server. Open **Events Manager -> your Web Event Set -> Events**, pick a recent `Purchase` (or any event the plugin sent with a customer phone on it), and look at the event's detail view: the parameters TikTok accepted are listed there, and phone appears under the **user** section. If `phone` is listed, matching is working; if the event arrived but no phone parameter is listed at all, the field is not reaching TikTok. The same view carries TikTok's own **Event Match Quality** score for the event set, which is the number that moves when phone matching starts working. If you use **Test Events** with a test event code, the same detail is visible there within a minute or two, without waiting for reporting to settle. (In 3.11.0 the plugin switched the phone and click-ID fields to the names TikTok's current Events API 2.0 reads — `user.phone` and `user.ttclid` — from the Events API 1.0 names it had been sending; if you were on an earlier version, this is the screen where the difference shows up.) = Why does the Excluded Traffic tab show IPs as `192.168.1.x`? = GDPR-friendly auditing — the last octet is masked at record-time, so wp-admin and DB exports never reveal raw visitor IPs. == External Services == This plugin connects your website to external services to send event data. * **Service Used:** Meta Conversion API (graph.facebook.com) * **Purpose:** To send user interaction and e-commerce event data from your server to Meta's servers for ad performance measurement, optimization, and audience building. * **Data Sent:** Event details (product ID, price) and user parameters (IP address, user agent, hashed email/name/phone, Facebook cookies) are sent when a user performs a key action. * **Service Used:** TikTok Events API (business-api.tiktok.com) * **Purpose:** Same as Meta CAPI, providing server-side conversion tracking for TikTok Ads optimization and attribution. * **Data Sent:** Event details (product ID, price, currency) and user parameters (IP address, user agent, hashed email/phone/external_id, ttp / ttclid cookies) are sent upon user action. Optional under the merchant's TikTok credentials — the plugin only sends to TikTok if the credentials are configured. * **Service Used:** Pinterest Conversions API (api.pinterest.com) * **Purpose:** Same as the Meta CAPI, providing reliable tracking for ad performance and audience building on Pinterest. * **Data Sent:** Event details and hashed user parameters are sent upon user action. * **Service Used:** Google Tag Manager (googletagmanager.com) * **Purpose:** To load a JavaScript container from Google's servers that allows you to manage and deploy marketing and analytics tags. * **Data Sent:** The plugin provides your GTM Container ID to Google to fetch the correct script. GTM itself may collect data based on how you configure your tags. * **Service Used:** Cloud-provider IP range list — `raw.githubusercontent.com/rezmoss/cloud-provider-ip-addresses` * **Purpose:** Used by the optional **Datacenter IP filter** to keep the bot blocklist current. Daily background fetch downloads CIDR ranges for AWS, Google Cloud, Azure, Cloudflare, DigitalOcean, Linode, Vultr, Oracle Cloud, and Fastly so events from those ranges can be filtered out before reaching Meta / Pinterest / TikTok. * **Data Sent:** None. The plugin only downloads public IP-range manifests; no visitor data is sent to GitHub. * **License:** Source repository is CC0-licensed. * **Service Used:** Apple iCloud Private Relay egress IP list — same `raw.githubusercontent.com/rezmoss/cloud-provider-ip-addresses` source (folder `apple_private_relay/`) * **Purpose:** Used by the optional **Datacenter IP filter** to whitelist real Apple visitors who exit through Apple's relay infrastructure. Daily background fetch downloads the merged CIDR list so iOS Safari users on Private Relay aren't mistaken for datacenter bots. * **Data Sent:** None. The plugin only downloads the public manifest; no visitor data is sent. **Shared hosting note.** Some restrictive shared hosts block outbound HTTPS by default. If event delivery silently fails after install, ask your host to whitelist the following domains for outgoing connections: `graph.facebook.com`, `business-api.tiktok.com`, `api.pinterest.com`, and `raw.githubusercontent.com` (only needed if you keep "Auto-fetched" enabled on the Blocked Traffic tab — covers both the datacenter blocklist and the Apple Private Relay whitelist). == Installation == ### Quick start (3 steps) 1. Install and activate the plugin. WooCommerce must already be active. 2. Open **CAPI Suite → Main Settings** and paste your **Meta Pixel ID + Access Token**. Add **TikTok** and/or **Pinterest** credentials if you use them. Empty fields for platforms you don't use are fine. 3. *(If you use GTM)* Download the bundled `gtm-template.json` from the **GTM Container ID** box, import it into your GTM container in **Merge** mode, set the pixel-code constants to your real IDs, and publish. Server-side events start flowing on the next page view. Send a test from **Event Management → Test Modes** to verify credentials before going live. ### Recommended GTM dedup configuration To prevent duplicate browser+server events: 1. In **Meta Events Manager → your Pixel → Settings → Event Setup**, turn **off** "Track Events Automatically Without Code". This plugin handles all event sending. 2. In your GTM container, pause or delete any auto-created tags starting with `FB_`. The bundled GTM template ships GA4 + Meta tags pre-wired to the GA4 ecommerce dataLayer, plus TikTok tags that read from a `CONST - TikTok Pixel Code` variable and Pinterest tags that read from `CONST - Pinterest Tag ID`. **The Pinterest tags are new, and one of them ships paused.** Earlier releases said Pinterest tags had to be built by hand because the Community Template could fail to import inside a container export. That is no longer true — it was re-tested against a real container, both as a Merge into an existing workspace and as a fresh import, and the Pinterest template and all eight tags came through intact — so they are bundled now. Seven of them are on by default: PageVisit, ViewContent, ViewCategory, AddToCart, InitiateCheckout, AddPaymentInfo and Search. Three of those (ViewContent, InitiateCheckout, AddPaymentInfo) are events the Conversions API has no name for at all, so the browser tag is the only way Pinterest ever hears about them. **`Pinterest - Checkout` is paused on purpose, and only you can finish the check.** Pinterest matches a tag event to a Conversions API event on `event_id`, and the tag passes it — but that pairing has not been confirmed on a live Pinterest account, and this plugin's author has no Pinterest ad account to confirm it on. Left paused, your Pinterest sales keep being reported from the server only, exactly as they are today, and cannot be counted twice. To finish the check: set `CONST - Pinterest Tag ID`, enable the tag, publish, make one real sale, and open Pinterest Events Manager. One `checkout` event means the pairing works and you can leave it on. Two means it does not — pause it again. One more thing if you report subscriptions separately: Pinterest's tag takes a fixed event name, so that tag sends `checkout` for a renewal too, while the server side deliberately reports renewals as `custom`. If you cannot import the JSON template (locked container, workspace permissions) or want to set up GTM manually, the full step-by-step walkthrough ships with the plugin at `wp-content/plugins/easy-meta-capi/docs/GTM-MANUAL-SETUP.txt`. ### Verify Open **CAPI Suite → Event Log** after browsing your store. Successful dispatches show as "Success (Meta)" / "Success (TikTok)" / "Success (Pinterest)". The Dashboard widget shows queue health at a glance. If the log stays empty, a JS optimizer is probably deferring the plugin's inline scripts — see the cache-plugin FAQ. Detailed GTM setup, Google Ads Enhanced Conversions, and other platform tags live in `docs/GTM-MANUAL-SETUP.txt`. Consent Mode v2, Strict server-side consent, CMP auto-block, and WC Subscriptions are documented under **Advanced Configuration** below. = Advanced Configuration = Setup details for Consent Mode v2, the strict server-side consent mode (GDPR PII gating), CMP auto-block compatibility, and the WooCommerce Subscriptions integration. None of these are required for a basic CAPI setup — turn them on as your store needs them. ### Consent Mode v2 Setup (GDPR / EU Compliance) If you serve EU visitors, Google's browser tags stop writing cookies when consent is denied — typically losing **20–50% of measured event volume**. Google Consent Mode v2 recovers part of it: when consent is denied, those tags switch to **cookieless pings** (anonymous beacons carrying event name, value, currency and timestamp but no client identifier), and Google models the conversions from them and shows them mixed with observed ones in your reports. **How to enable.** Popular CMP plugins (Cookiebot, CookieYes, Complianz, Iubenda, Termly, OneTrust) all have a native Consent Mode v2 toggle in their settings — find and enable it. The CMP then calls `gtag('consent', 'default', {denied})` before GTM loads and `gtag('consent', 'update', {granted})` after the visitor accepts. **If you use a consent banner, import the container again.** The container shipping with this release stops the Meta, TikTok and Pinterest tags from firing for visitors who declined: all 23 of them now wait for `ad_storage` and `ad_user_data` before they run. Download it again and import it as described above, choosing **Merge** and then **Overwrite conflicting tags, triggers, and variables** — overwrite is what replaces the tags you already have. Tags you built yourself, under names the template does not use, are left alone. The same import returns the template's ID constants (`CONST - Meta Pixel ID`, `CONST - GA4 Measurement ID`, `CONST - TikTok Pixel Code`, `CONST - Pinterest Tag ID`, `CONST - Google Ads Conversion ID`, `CONST - Google Ads Purchase Label`) to their placeholder text, so enter your own values again before you publish. `CONST - Google Ads Conversion ID` now takes the number on its own — `1234567890`, not `AW-1234567890` — because the Google Tag in the template adds the `AW-` prefix itself. The bundled GTM template includes a paused **"Consent Defaults (Pre-CMP)"** tag. Enable it only if your CMP doesn't set `gtag('consent', 'default', ...)` on its own (rare with modern CMPs). ### Strict server-side consent mode (PII gating for CAPI) Consent Mode v2 only controls **browser** tags. Server-side CAPI fires from PHP, never sees `gtag('consent', ...)` signals — so it transmits hashed PII regardless of cookie-banner choice. Fine outside the EU; a GDPR concern inside it. The **Privacy & Consent (Server-side)** section has a Strict server-side consent toggle (default OFF). When enabled and the visitor has denied marketing consent in your CMP, identifying PII (`em`, `ph`, `fn`, `ln`, address, `fbp`, `fbc` …) is stripped from the CAPI payload. The event still ships with `event_id`, `value`, `currency`, `contents` — Cookiebot, CookieYes, and Complianz cookies are read automatically; other CMPs supply state via the `mcapi_marketing_consent_granted` filter. **Whose answer is used.** A Purchase usually fires later than the checkout it belongs to — when the order reaches a paid status, which for a redirect gateway can be hours or days, in a cron tick or a gateway webhook where the shopper's browser is nowhere in sight. The shopper's consent answer is therefore recorded onto the order at checkout (both the classic and the block checkout) and read back from there at send time. That is what makes the toggle honour the customer's own choice rather than whoever's browser happened to trigger the send — a shop manager working in wp-admin, for instance, whose own banner choice used to decide it. An order placed before this version, or created without a checkout, has no recorded answer and is treated as "no signal", the same as a visitor with no CMP cookie; a renewal inherits the answer given at the original purchase. **Why this matters alongside Consent Mode v2.** Denied-consent browser pixels switch to cookieless pings — modeled, not observed. With Strict server-side consent ON, your server-side CAPI ships alongside that ping carrying the same `event_id`. Meta dedupes by `event_id` and now has an **observed** server signal feeding the same conversion record the cookieless ping created — cleaner Event Match Quality than browser-only or naïve "send everything" CAPI, and GDPR-defensible because no identifying data leaves your server. Default OFF preserves match quality for existing non-EU setups. Recommended ON once Consent Mode v2 is configured in your CMP. ### CMP Auto-Blocking and the Plugin's Inline Scripts CMPs with "auto-blocking" (Cookiebot, CookieYes, others) scan every `