CAPI Suite: Meta, Pinterest, TikTok, GTM

Descrição

Stop paying $30–150/month for a GTM Server Container. Send Conversions API events to Meta, Pinterest, and TikTok directly from your WordPress server. No premium tier, no SaaS subscription.

Three CAPI integrations in one install. Most plugins ship Meta only, or sell Pinterest and TikTok as paid add-ons. This one runs server-side dispatch to all three plus a bundled GTM template for GA4 + Google Ads (Enhanced Conversions, Conversion Linker, click-ID recovery for iOS Safari post-ITP). The same event_id flows browser-side and server-side so each platform deduplicates instead of double-counting.

Aggressive bot filtering, no false-positives on real customers. Layered detection (behavioral signals + ~9,500-CIDR datacenter list + AI-crawler classification for GPTBot / PerplexityBot / ClaudeBot / Google-Extended / Bytespider / etc.) blocks Lighthouse audits, scrapers, and ad-fraud bots. Multi-layered customer bypass — Apple iCloud Private Relay whitelist, ad-click ID recognition (fbclid / gclid / ttclid), logged-in customers, prior-visit _fbp / _ga cookies, Cloudflare Bot Management validation — keeps real shoppers visible. Purchase events are never blocked; pre-Purchase events held by the filter are replayed on eventual checkout so Meta sees the complete funnel.

  • Server-side CAPI to Meta, Pinterest, TikTok — every checkout & cart event, classic + block-based checkout, HPOS compatible. Per-platform retry isolates transient failures.
  • GTM template for browser-side tags — GA4 + Meta Pixel + Pinterest Tag + TikTok Pixel + Google Ads (Enhanced Conversions enabled, Conversion Linker auto-attached).
  • Event Log with By-IP view — paginated audit log, customer-protection badges prevent excluding real buyers, one-click exclude for confirmed bots, per-provider breakdown.
  • GDPR / CCPA modes — honors CMP opt-out signals, strict server-side consent strips PII when consent is denied (browserCAPI dedup still works on event_id).
  • Cache-safe — works with LiteSpeed, WP Rocket, Varnish, Cloudflare full-page cache. Click IDs captured into 1st-party cookies client-side; landing pages stay cacheable.
  • WP Dashboard widget — queue health at a glance: backlog, oldest pending, last successful dispatch, datacenter exclusions today.

If it helps your store, please leave a review — it genuinely helps other merchants find this plugin.

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).

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, GA4 and Meta browser tags don’t fire when consent is denied — typically losing 20–50% of measured event volume. Google Consent Mode v2 recovers this: when consent is denied, GA4 / Meta tags switch to cookieless pings (anonymous beacons carrying event name, value, currency, timestamp but no client identifier). Google’s ML models the conversions from these pings and shows them mixed with observed ones in your reports. A single CMP integration repairs both GA4 and Meta attribution because the Meta Pixel template reads the same consent signals.

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.

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.

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 <script> tag on load and convert anything they suspect of tracking to type="text/plain" until consent. The plugin’s inline scripts only POST first-party events to your own REST endpoint — but a generic auto-blocker can’t tell. To avoid a silent break, every plugin-rendered inline script ships with opt-out attributes for Cookiebot (data-cookieconsent="ignore"), CookieYes (data-cookieyes="cookieyes-necessary"), and Complianz (data-cmplz-no-cookielaw="1"). For other CMPs (OneTrust, Quantcast, in-house), append your own attribute via the mcapi_inline_script_attrs filter.

WooCommerce Subscriptions Integration

By default, every WooCommerce Subscriptions auto-renewal sends a fresh Purchase to Meta CAPI — credited to the original acquisition ad. Reported ROAS keeps climbing month after month from the same conversion, polluting optimization signals.

The plugin auto-detects WooCommerce Subscriptions and exposes:

Subscription Renewal Behavior (radio):

  • Default — renewals send as regular Purchase. Existing setups unchanged.
  • Skip — renewals not sent. Cleanest ROAS hygiene; you forfeit Meta’s LTV signal from renewals.
  • Tag — renewals still send Purchase but with custom_data.customer_status = "subscription_renewal" so you can filter them in Events Manager.
  • Subscribe / SubscriptionRenewal events — Meta’s standard Subscribe for sign-ups + a SubscriptionRenewal custom event for renewals. Purchase stays clean, advertisers using LTV-bidding can opt into both.

Tag every Purchase with customer_status (checkbox): adds custom_data.customer_status (new_customer / returning_customer / subscription_renewal) to every Purchase so Meta Advantage+ can bid acquisition vs. retention differently. Guest checkouts fall back to billing-email lookup.

X-Forwarded-For Hop Count (advanced proxy setups)

Behind a trusted reverse proxy, the real-IP resolver walks X-Forwarded-For from the right, skipping entries that look like trusted infrastructure (a known proxy CIDR or a private/reserved address), and uses the first one that doesn’t. This defeats the common attack of forging a leftmost entry, but it has a residual gap: if your proxy is configured to pass X-Forwarded-For through unmodified instead of appending the hop it actually witnessed (nginx’s proxy_set_header X-Forwarded-For $http_x_forwarded_for instead of $proxy_add_x_forwarded_for is the classic case), every entry in the header is client-controlled — a visitor can append a filler value that merely resembles infrastructure (e.g. a private-looking address) and have an earlier, forged entry accepted as their IP instead.

The plugin cannot detect this misconfiguration or infer your real proxy hop count on its own. If you know it — one hop for a single reverse proxy, more for a CDN plus a local proxy — set it with the mcapi_trusted_proxy_hops filter:

add_filter( 'mcapi_trusted_proxy_hops', function() { return 1; } );

This bounds how many trailing hops the walk will ever treat as trusted-and-skippable; once that many are skipped it stops rather than continuing further left, so a crafted filler hop beyond the boundary can no longer push an attacker-chosen address through. Default is unbounded (today’s behaviour) — leave it unset unless you are certain of your topology, since too low a value can make the walk stop before reaching a real client hop.

Disclaimer

This plugin is an independent, community-driven implementation of server-side Conversions API protocols. It is not affiliated with, endorsed by, or sponsored by Meta Platforms, Inc., TikTok Ltd., Pinterest, Inc., Google LLC, Automattic Inc., or any other trademark holder referenced herein.

“Meta”, “Facebook”, and the Meta Pixel are trademarks of Meta Platforms, Inc. “TikTok” is a trademark of TikTok Ltd. “Pinterest” is a trademark of Pinterest, Inc. “Google Tag Manager”, “Google Ads”, and “GA4” are trademarks of Google LLC. “WooCommerce” is a trademark of Automattic Inc. All trademark references are used solely for descriptive interoperability purposes — to indicate which platforms this plugin can transmit data to under the merchant’s own configured credentials.

No user data is transmitted to any external service until the merchant explicitly configures their own platform credentials in the plugin settings. The plugin does not “phone home” or contact any developer-controlled server. The only outbound HTTP calls are: (1) merchant-configured CAPI endpoints, (2) the public CIDR manifests at raw.githubusercontent.com used by the optional Datacenter IP filter — no visitor data is sent in those manifest fetches.

Instalação

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. Pinterest tags are added manually because the Community Template can fail to import inside container exports.

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.

FAQ

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 you use other GTM plugins (like Google Site Kit), disable their e-commerce features to avoid conflicts.

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 four behavior modes (Default / Skip / Tag / 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 browserCAPI 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.

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.

Avaliações

Leia 1 avaliação

Contribuidores e desenvolvedores

“CAPI Suite: Meta, Pinterest, TikTok, GTM” é um software com código aberto. As seguintes pessoas contribuíram para este plugin.

Contribuidores

Registro de alterações

3.8.1

  • Fix: purchases paid after checkout reported the server’s own IP address. When an
    order reaches a paid status later — the normal path for a redirect payment gateway —
    the event is sent from a background task where the shopper’s request no longer exists,
    and the plugin was reading the address and browser from that task instead of from the
    order. WooCommerce records the real ones at checkout and they are now used. These two
    fields are what the ad platforms need to connect an event to a browser at all, so a
    wrong value cost more than a missing one.
  • Critical: an order updated from wp-admin, cron, or WP-CLI could pick up whoever’s
    own browser cookies happened to be present.
    Click identifiers, the ad-platform
    pixel identifiers, and the guest visitor identifier are now read from the live
    request only when that request can genuinely belong to the shopper. The normal path
    once a redirect gateway, a manual bank transfer, or a subscription renewal later
    moves an order to a paid status runs with no shopper present at all — previously
    that request’s own cookie jar (a merchant’s, in wp-admin) was read as if it were the
    customer’s, attaching the wrong person’s browsing identity, and even their own guest
    visitor ID, to someone else’s purchase.
  • Phone numbers typed without a country code are now interpretable. Such a number is
    read against the billing country when known, otherwise your store’s own base country,
    otherwise a network hint — and left exactly as typed when none of those is available,
    rather than guessed at. A number already written in international form is never
    altered.
  • Facebook Login user IDs are sent where your store offers that sign-in, and always
    for the real buyer.
    This is the ad platform’s own identifier for the person, so it
    matches directly instead of probabilistically. Read from the common social-login
    plugins, with a filter for others. It is always sourced from the order’s own customer,
    never from whoever happens to be signed into your admin at the time — so a staff
    member updating an order’s status in wp-admin can never have their own account
    attached to someone else’s purchase.
  • Subscription renewals now carry the subscription’s own ID, giving the ad platform a
    stable handle on the recurring relationship behind each charge.
  • Each event now sends a second identifier derived from the customer’s email alongside
    the existing one, which helps the ad platform recognise a returning shopper sooner.
    Also fixes a guest checkout that could send that same address twice — once in a form
    that matched, once in a form that could never match anything.
  • Stores collecting gender or date of birth can supply them through the new
    mcapi_identity_extra_fields filter.
  • The identifiers and details added in this release — sign-in ID, subscription ID,
    gender, and date of birth — are all removed from the event, exactly like every
    existing identifying field, when a visitor has declined marketing consent under
    Strict server-side consent mode. This protection also now reaches TikTok’s own
    click and pixel identifiers, previously missing from that list, and events that
    were held back for delayed delivery because the visit looked like datacenter
    traffic — those are stripped before they are held, not only if and when they are
    eventually sent.

3.8.0

  • Critical: GTM template re-import required — TikTok double-counting otherwise. TikTok
    realigned its event names in Aug/Sep 2025 (CompletePayment Purchase, SubmitForm
    Lead); the server-side CAPI call now sends the current name, but the bundled GTM
    template’s TikTok Purchase tag still sent the retired CompletePayment name until this
    release. TikTok deduplicates browser and server events by event name plus event_id — with
    the two sides disagreeing on the name, TikTok never matched them as duplicates, so every
    TikTok purchase reports twice
    (once from the browser tag, once from the server) until
    you re-import
    . Re-download gtm-template.json from Main Settings and re-import your GTM
    container in Merge mode. If you cannot re-import, edit the “TikTok – Purchase” tag’s
    Event Name field to Purchase manually (see docs/GTM-MANUAL-SETUP.txt). Server-side CAPI
    dispatch is otherwise unchanged.
  • Purchase is now gated on order status, with late firing. The event used to fire on the
    order-received page regardless of order status, so a customer who abandoned payment at a
    redirect gateway still produced a confirmed Purchase in Meta. It now fires when the order
    reaches a status you count as a sale — configurable under Event Management, defaulting to
    whatever WooCommerce and your other plugins treat as paid, plus On hold for bank transfers.
    If the order gets there later, the event is sent at that moment via
    woocommerce_payment_complete / woocommerce_order_status_changed, so an order paid by
    transfer days later is still reported even though the customer never came back — that case
    previously sent nothing at all. An order-meta flag prevents duplicates, and the browser
    dataLayer push is gated by the same 48-hour window so browser and server events stay
    paired instead of being counted twice — see the dedup-window fix below for how that
    window is computed for orders placed before this release.
  • Fix: one stale queued event could discard an entire batch. Meta rejects a request
    containing any event_time older than 7 days and fails the whole request. Deferred-event
    replay could re-queue a row up to ~8 days old, and the resulting HTTP 400 was classified as
    permanent — dropping every event in a batch of up to 500. Timestamps are now clamped to 6
    days at dispatch.
  • Fix: the Event Log’s Refresh button did nothing on the default view. The page caches its
    query under a key that includes the active date range, and it applies a default 7-day range
    on every load — but cache invalidation only ever cleared the un-ranged key, so newly sent
    events stayed hidden for up to 60 seconds and the Refresh Log button had no effect. Key
    construction now lives in one place used by both the page and the invalidator. These
    transients are also cleaned up on uninstall for the first time.
  • Fix: Cloudflare bot score was never read, and the readme said otherwise. Cloudflare does
    not forward its bot score to the origin on any plan; it needs a Transform Rule, Snippet, or
    Worker. The plugin now reads X-Bot-Score (Cloudflare’s documented example name) as well as
    Cf-Bot-Score, filterable via mcapi_cf_bot_score_headers, and the readme no longer claims
    Enterprise plans send it automatically.
  • Fix: a malformed mcapi_cf_bot_score_threshold filter return could bypass the bot filter
    entirely.
    The threshold was applied with a plain (int) cast, so a filter that returned
    null, an empty string, or anything non-numeric silently became 0 — and any real
    Cloudflare score (1-99) is always >= 0, making the check trivially true. Only affects
    sites that both forward a bot-score header and use a filter that can misbehave. A malformed
    return (non-numeric, or numeric but outside 1-99) now falls back to the documented default
    of 30 instead — a broken filter behaves as if no filter were registered, never weaker or
    stronger than that. Numeric strings (e.g. '50') are still accepted as valid thresholds,
    matching how the bot-score header itself is read.
  • Fix: block checkout reported wrong values in zero- and three-decimal currencies. The cart
    total was divided by a hardcoded 100 instead of by the currency_minor_unit the Store API
    reports, so JPY / KRW / CLP stores reported 1/100th of every value and KWD / BHD stores ten
    times too much.
  • Fix: block checkout sent no event_source_url, so events were attributed to
    /wp-admin/admin-ajax.php and Event Match Quality suffered. It now also uses the REST
    endpoint with the behavioural proof piggyback, matching classic checkout.
  • Fix: TikTok events were lost when the queue was unavailable. The synchronous fallback
    covered Meta and Pinterest only.
  • Fix: the “Update datacenter list now” notice always reported 0 ranges, and the
    downloadable list tagged every range bundled/custom. Both read options that stopped being
    written in 3.6.0.
  • Fix: mcapi_update_apple_relay_ips survived deactivation as an orphan daily task. All
    recurring hooks now come from one roster used by activation, deactivation, and the watchdog
    below.
  • Fix: uninstall left residue — two options, one user-meta dismissal flag, and the by-IP
    log transients added in 3.7.0.
  • Login and Comment events are now controllable. Both fired unconditionally and were
    missing from the Event Log filter. They remain enabled by default; unchecking them in Event
    Management now works.
  • Fix: blocklist manager pagination. With rows in both IPv4 and IPv6 tables, page 2 onward
    showed the wrong rows and IPv6 entries were unreachable until IPv4 was exhausted. Replaced
    with a single UNION ALL query.
  • New: scheduler watchdog. If a recurring background task goes missing — the 3.7.1 failure
    mode that silently halted all dispatch — it is now rescheduled automatically on the next
    admin page load instead of waiting for a plugin update.
  • Meta Graph API v26.0 (from v25.0). Pin it back from wp-config with
    define(‘MCAPI_FB_GRAPH_VERSION’, ‘v25.0’); if needed.
  • TikTok event names updated to Purchase and Lead, replacing the legacy
    CompletePayment and SubmitForm that TikTok renamed in 2025. ClickButton and
    PlaceAnOrder are unchanged — they are soft-deprecated but still standard events and carry
    optimisation signal through 2027.
  • Fix: the exclusion list called ranges “Active” that it was not consulting. Switching a
    segment off in Active source list stopped it being used for lookups, as documented, but
    every count and status badge on the page still described its ranges as active — so a store
    that had disabled the auto-fetched list was told thousands of ranges were in force while
    none were. Counts are now source-aware, and a stored-but-unconsulted row reads Source
    off
    instead of Active.
  • Tested against WordPress 7.0 and WooCommerce 11.0 (Action Scheduler 4.0).
  • Removed dead code: the unused datacenter CIDR textarea renderer and its orphaned sanitizer.
  • Admin assets are versioned by file timestamp, so a stylesheet fix within one release can no
    longer be masked by a browser’s cached copy.
  • Fix: identity fields were not normalized to Meta’s specification before hashing.
    City kept its spaces (new york instead of newyork), postcodes kept spaces and
    dashes (sw1a 1aa instead of sw1a1aa), US ZIP+4 was sent in full instead of the
    leading five digits, and names kept punctuation (o'brien instead of obrien).
    Meta accepts such fields and counts them toward match coverage, but the hash matches
    nobody — the signal was discarded without any visible symptom. Empty and
    punctuation-only fields are no longer hashed either; that produced a valid-looking
    digest matching nobody. Affects Meta, TikTok and Pinterest alike.
  • Fix: an order paid after checkout kept its ad-click attribution. Purchase now fires
    when an order reaches a paid status, which for a redirect payment gateway is commonly
    hours or days later, from a background request with no browser cookies. Only the two
    Meta cookies were stored on the order, so such a Purchase would have reached TikTok with
    no ttclid and Pinterest with no click ID. Every click identifier available at checkout
    is now stored and read back. Applies to orders placed after this update — earlier orders
    never captured the values.
  • Subscription renewals inherit the original purchase’s click identifiers. Renewal
    orders are created without a browser session, so they would otherwise carry no click
    attribution at all.
  • Fix: a returning visitor could double-count a sale. The order-received page stays
    reachable by order key. Revisiting it more than 48 hours after the event was reported
    produced a browser event Meta could no longer deduplicate against the server one,
    counting the sale twice. Orders placed before this update recorded no dispatch time at
    all — the field is new here — so the window is approximated from the order’s own paid
    date for those, meaning the protection covers your existing order history too, not only
    orders placed from now on.
  • Fix: X-Forwarded-For trusted the client’s own forged leftmost entry. Behind a
    trusted reverse proxy, the address our infrastructure actually observed is appended to
    the right of the header — the leftmost entry is whatever the visitor’s own browser or
    script chose to send. The real-IP resolver now walks the header from the rightmost
    entry, skipping any that are themselves a trusted proxy or a private/reserved address,
    and returns the first one that is neither, falling back to REMOTE_ADDR if every entry
    is. This closes a path for a visitor to pick their own apparent IP and bypass the
    datacenter/bot filter or poison the behavioural IP-state identity. Only the generic
    trusted-proxy path is affected; sites relying solely on Cloudflare’s
    CF-Connecting-IP were never exposed to this.
  • Fix: shared hosts without an object cache had no site-wide rate limiting at all.
    The per-IP limiter is deliberately skipped without Redis/Memcached, to avoid flooding
    wp_options with one new row per attacker IP under an L7 flood — but the single-row,
    per-minute-bucketed global limiter that exists precisely for that case was opt-in and
    defaulted off, so such sites had no cap whatsoever. It is now on by default whenever
    there’s no object cache. The existing mcapi_force_global_rate_limit filter still lets
    a merchant turn it off, and mcapi_global_rate_limit_per_minute still overrides the
    2000/minute cap.
  • Fix: the Contact Form 7 Lead event hashed the submitted email by hand, bypassing the
    shared identity-field pipeline every other field goes through before hashing. Stray
    whitespace or mixed case hashed to a digest that could never match Meta’s own copy, and a
    value the pipeline would reject was hashed anyway, sending the well-known
    SHA-256-of-empty-string digest as if it were real data. The lead email now flows through
    that same pipeline and is omitted entirely rather than hashed when nothing usable remains.
  • Fix: event_id fallbacks used bare uniqid(), which has microsecond resolution. Two
    events generated by concurrent PHP workers in the same microsecond — plausible on a busy
    store — could get the same event_id; Meta, TikTok, and Pinterest all deduplicate by
    event_id, so a collision silently merged two distinct conversions into one, with no visible
    error. Every fallback now uses a UUIDv4, keeping its existing human-readable prefix for the
    Event Log.
  • Fix: a long User-Agent could be truncated mid-character before being logged. The
    500-character column limit was enforced with a byte-based substr(), which can split a
    multibyte UTF-8 character in half. Now truncated with mb_substr() where available.
  • New: mcapi_trusted_proxy_hops filter bounds the X-Forwarded-For hop walk. If your
    upstream proxy passes the header through unmodified instead of appending its own witnessed
    hop, every entry is client-controlled, and a crafted filler hop that merely looks like
    infrastructure could still be skipped past to reach an attacker-chosen address. Setting this
    filter to your real proxy hop count stops the walk at that boundary instead. Default is
    unbounded — behaviour is unchanged unless you set it. See X-Forwarded-For Hop Count in
    Advanced Configuration.

3.7.2

  • Hotfix: queue processor timeout on bursty event flushes. A regression in 3.7.1 could leave the recurring mcapi_process_event_queue Action Scheduler job marked as “failed after 300 seconds” when a single visitor’s accumulated deferred events flushed into one cron tick. Once failed, the action wasn’t re-scheduled — silently halting all dispatch to Meta/Pinterest/TikTok until the plugin was deactivated/reactivated. Two changes: (1) per-event admin-cache invalidation moved out of the queue’s hot loop and consolidated to a single call at batch end (was up to 28 delete_transient() calls per event per platform on bloated wp_options tables — sufficient to push 100-event batches past Action Scheduler’s wall on slower hosts); (2) deferred-event recovery now caps per-call drain at 50 rows (filterable via mcapi_deferred_recover_limit) so a single trust graduation can’t dump a saturated backlog into one tick. Affected sites auto-recover on update — no manual intervention needed.
  • Cloudflare Bot Management score header. If you forward Cloudflare’s bot score to your origin, the datacenter-IP behavioral filter treats a score of 30 or higher (1–99, higher = more human-like) as a customer-bypass signal alongside the existing __cf_bm cookie check. Cloudflare does not send this score automatically on any plan — you add it yourself with a Transform Rule, Snippet, or Worker, and the plugin reads X-Bot-Score (Cloudflare’s documented example header name) or Cf-Bot-Score. Sites that haven’t configured it are unaffected: the __cf_bm cookie check covers every Cloudflare tier.
  • navigator.webdriver gate on behavioral proof. Selenium / Puppeteer / Playwright default configurations set navigator.webdriver = true. The keepalive heartbeat now suppresses proof emission when the flag is on, so automation-driven sessions can’t accumulate behavioral trust on the server even if they happen to also satisfy the existing isTrusted gate. Patchable by hostile bot authors (stealth plugins exist), but each bypass is real engineering cost — same cost-moat principle as the rest of the layered detection.
  • human_score >= 100 strong-trust now used. The mcapi_ip_state.human_score column has existed since 3.6.0 but wasn’t read anywhere — flagged as dead code in code review. It now contributes to the Event Log customer-protection logic: an IP that graduated to trusted via multiple confirmations (keepalive + behavioral proof, summing past 100) qualifies as a Customer signal on its own (same bar as a Purchase event), reducing false-positive Exclude clicks on long-engaged real shoppers.
  • Description text refresh. Tightened the plugin-page description: clearer Google Ads framing (browser-side via GTM template, not a CAPI integration), Apple Private Relay and Cloudflare Bot Management surfaced in the customer-bypass list, removed three “free” repetitions and the redundant “What it does” header, normalized bullet lengths.

3.7.1

  • AddToCart dataLayer push fix. On themes that don’t render a WooCommerce cart widget on every page (custom themes, headless setups, some page builders), the browser-side add_to_cart event was silently lost because WC’s $(selector).replaceWith() is a no-op when no matching DOM element exists. The handler now reads the payload from the fragments argument WC passes to the added_to_cart event — works regardless of DOM state. Adds a shared window.__mcapi_atc_pushed dedup map so any custom GTM Custom HTML tag using the same name (a known forum workaround pattern) no longer causes double-pushes. Server-side CAPI dispatch was working in all cases and is unchanged.
  • Behavioral bot filter — burst protection tightened. A pacing check (mcapi_min_event_gap filter, default 3 seconds average gap between events) now gates soft-trust promotion. Previously an IP firing exactly 3, 4, or 5 events in under a minute slipped through both the burst-protection floor (which only triggered above 5 events) and into trusted state. Borderline-fast clickers fall through to defer (not block) and re-evaluate as they accumulate more events.
  • By-IP transient cache invalidated on Refresh Log. The “Refresh Log” button now also clears the mcapi_logs_byip_* transients introduced in 3.7.0; previously stale by-IP data could persist for the full 60-second TTL after refresh. Per-insert cache invalidation also updated so by-IP view refreshes immediately on new events.
  • Admin hygiene: the one-time GTM-3.5.0 notice flag no longer autoloads on every request.

3.7.0

  • Event Log: By-IP grouped view. New toggle on the Event Log tab switches between detail rows and a per-IP aggregate (IP | Events | User Agent | Event types | Action) sorted by hit count descending. Repeat-offender IPs surface immediately — bulk-exclude with one click instead of scrolling through hundreds of individual rows. Filter form preserves the active view + page anchor on submit.
  • Customer-protection badges in the Event Log. Before excluding an IP, the plugin checks for real-visitor signals: Purchase events (financial transaction — bots can’t complete a real checkout), checkout-flow events (InitiateCheckout / AddShippingInfo / AddPaymentInfo), behavioral filter graduation, and funnel-event diversity. Multi-signal scoring: a Purchase alone qualifies, otherwise 2+ signals are required. Real buyers show a 🛒 Customer badge and the Exclude button is suppressed; if a customer was previously excluded by mistake, a ⚠🛒 warning appears so you can undo it.
  • AI-crawler classification (separate from generic bots). GPTBot, ChatGPT-User, OAI-SearchBot, PerplexityBot, ClaudeBot, anthropic-ai, Google-Extended, GoogleOther, Applebot-Extended, FacebookBot, Amazonbot, CCBot, Bytespider, and other LLM training / answer-engine crawlers are now classified as ai_agent instead of bot. Requests to /.well-known/ discovery endpoints (UCP / llms.txt / ai-plugin.json) are also treated as AI agents. They are skipped from CAPI dispatch like bots, but tracked in a separate daily counter on the Dashboard widget so you can see how much LLM-driven traffic reaches your store. Never added to the IP exclusion list. Extendable via the mcapi_ai_agent_user_agents filter.
  • “Block” “Exclude” UI rename. The Blocked Traffic tab is now Excluded Traffic, the exclusion-list buttons are clearer, and a banner at the top of the tab spells out that this is a CAPI-event-level exclusion, not a firewall — your site stays accessible to everyone, only the plugin’s analytics dispatch is filtered for those IPs. Manage-list rows use Disable / Enable toggles instead of Exclude / Re-include (less terminology collision). Internal hook names and DB option keys unchanged — backward compatible.
  • Flagged-IP indicator. IPs that tripped the behavioral filter (honeypot, abnormal velocity, or no engagement after observation) now show a 🚩 indicator on the Exclude button with a confidence-aware tooltip. Mostly catches bots that disguised their User Agent to slip past the upstream filter.
  • EU consent admin notice. When a CMP plugin is detected (CookieYes / Cookiebot / Complianz / Iubenda / Termly) and Strict server-side consent mode is OFF, the settings page now shows a one-time dismissable prompt explaining the GDPR posture and pointing to the toggle.
  • Trademark disclaimer. Added a Disclaimer section to the readme covering independent / not-affiliated status for Meta, TikTok, Pinterest, Google, and Automattic — descriptive interoperability use only.
  • Schema migration: mcapi_logs.ip_hash. New salted-SHA256 column (matches mcapi_ip_state.ip_hash) lets the Event Log JOIN against the behavioral-state table for real-time customer / flagged classification. Legacy rows have ip_hash='' and fall back to event-based signals. Single dbDelta ALTER TABLE on upgrade — instant DDL on InnoDB 5.7+ / MariaDB 10.3+.
  • Readme cleanup. Cookie-plugin setup, CMP auto-block, Strict consent, and WC Subscriptions sections de-duplicated and condensed. Two-thirds shorter without losing any why-this-matters context.
  • Google Ads Enhanced Conversions. Bundled GTM template now includes a Google Ads Conversion Linker tag (All Pages) and a Google Ads Purchase Conversion tag (CE – Purchase trigger) with Enhanced Conversions enabled in AUTO mode. The plugin captures gclid, gbraid, and wbraid from ad-click URLs into 1st-party cookies (_mcapi_gclid etc.) at landing; the Conversion Linker transfers these to _gcl_aw for Google Ads attribution. Recovers conversions that iOS Safari ITP would otherwise drop. Edit two new CONST variables in GTM after importing (Google Ads Conversion ID, Google Ads Purchase Label) — see docs/GTM-MANUAL-SETUP.txt for full setup.
  • Pinterest EMQ improvements. Now captures both Pinterest’s persistent _epik cookie (set by Pinterest’s tag.js on real visitors) and the epik URL parameter. Improves Event Match Quality on Pinterest tag installs running the current (2024+) version, where the legacy pina_id flow is being phased out.
  • Stronger real-user signals in datacenter bypass. Beyond _fbp and _ga, the IP filter now also accepts _epik (Pinterest tag), __cf_bm (Cloudflare Bot Management actively-validated browsers), _gcl_au (gtag.js ran), and _ttp (TikTok Pixel) as proof of human browsing. Reduces false-positive blocking of VPN/Apple-Relay shoppers who already have one of these tag cookies set.
  • Improved bot / human differentiation. Behavioral signals (mousemove, scroll, checkout-form interaction) are now gated against the most common scripted-automation patterns. Combined with the existing datacenter IP filter and funnel-event history, this reduces false-positive bot scores from real shoppers and false-negative human scores from low-effort scrapers. Not a 100% bot block — it raises the cost for an attacker to look human, not eliminates the possibility — but it filters out the bulk of the cheap traffic that pollutes Events Manager.
  • gbraid / wbraid capture. Google Ads iOS Safari click variants are now captured into 1st-party cookies alongside gclid. Without this, post-ITP iOS Safari ad clicks lose attribution within minutes.
  • GTM template re-import recommended. The bundled template now includes Google Ads tags. Re-download gtm-template.json from Main Settings and re-import in Merge mode.

3.6.0

  • TikTok CAPI integration. Server-side dispatch alongside Meta and Pinterest. Pixel Code, Access Token, Advertiser ID, and a dedicated TikTok Test Mode. Re-import the GTM template to get TikTok Pixel tags.
  • Behavioral bot detection. Datacenter IP visitors are briefly observed before forwarding events. Real-browser activity (mouse/scroll, _fbp cookie, click IDs, Apple Private Relay, logged-in customers) graduates the visitor instantly; confirmed bots are dropped. Purchase events are never blocked.
  • Blocklist redesign. Pre-bundled ~9,500 cloud-provider CIDR ranges with daily auto-refresh. IPv4 + IPv6 support, O(log N) lookup via binary index seek. New Blocked Traffic admin tab with per-source toggle (bundled / auto-fetched / custom), paginated table, and one-click “Block this CIDR” on Event Log rows.
  • Funnel-chain recovery. Held pre-Purchase events are replayed on the next Purchase from the same visitor (PageView ViewContent AddToCart InitiateCheckout), so Meta sees the full attribution path instead of a lone Purchase.
  • Apple Private Relay whitelist. Daily-fetched egress IPs bypass the datacenter filter, preserving iOS shopper events.
  • CCPA / Limited Data Use toggle. Honors visitor opt-out via cookie or filter.
  • Synchronous / Asynchronous sending modes. Synchronous (3-second per-platform timeout) for shared hosts where cron is unreliable.
  • WP Dashboard widget. Queue health at a glance: size, oldest pending age, last successful dispatch, datacenter blocks today.
  • Per-platform retry. When Meta succeeds but Pinterest or TikTok transiently fails, only the failing platform is retried next cron tick.
  • Critical fix: queue processor no longer leaks rows when an event’s send_to targets a platform with no credentials configured. Previously such rows could accumulate indefinitely (tens of thousands over days). Now correctly dropped on the first cycle.
  • Security: REST endpoint requires an HMAC-rotated token with a 25-hour tolerance window covering HTML page caches. Checkout-funnel honeypot rejects empty-cart fake POSTs. IP hashes salted with wp_salt('auth') for GDPR/KVKK compliance. Proxy headers trusted only when REMOTE_ADDR is in a known proxy range.
  • Performance: chunked DELETE for log/queue cleanup. Composite B-tree index for binary blocklist seek. Negative cache on visitor lookups. REST rate limiter skipped on installs without a persistent object cache. Ad-click landing pages no longer force-create a WooCommerce session.
  • Plugin renamed to “CAPI Suite: Meta, Pinterest, TikTok, GTM”. Settings UI reorganized: Sending Method + Test Modes moved to Event Management tab.
  • GTM template updated to modern API schema with TikTok Pixel tags. Re-import required.

3.5.3

  • Fix: spurious AJAX add_to_cart events from WooCommerce sessionStorage fragment replay.
  • Fix: per-platform retry tracking — when one platform transiently fails, only the failing side retries.
  • New: Event Log captures User Agent, supports date-range filtering, and retention is configurable (1–90 days, default 15).
  • Hardening: third-party autoloader protection extended to all class_exists() calls.

3.5.2

  • Critical: GTM template re-import required. Full migration to modern GTM API schema (older templates rejected with “File format invalid” / “Unknown entity type” in fresh workspaces). Plugin runtime unchanged.

3.5.1

  • Critical hotfix: CMP detection helper triggered third-party autoloader fatals (CookieYes / Cookie Law Info). All detection class_exists() calls now pass false to suppress autoload.

3.5.0

  • Fix: GTM container template imports cleanly (was rejected with “Unrecognized value [customEvent]”).
  • New: Consent Mode v2 support, CMP auto-block exemption (CookieYes / Cookiebot / Complianz), and a CMP detection admin notice.
  • New: Strict server-side consent mode — strips hashed PII when consent denied; still ships event_id + non-PII context for dedup.
  • New: WooCommerce Subscriptions integration — Subscription Renewal Behavior + customer_status tagging keep Purchase ROAS clean for subscription stores.
  • Fix: _fbp / _fbc cookie domain strips leading www. to match Pixel JS.

3.4.2

  • Fix: GTM template adds two CJS variables converting GA4-schema dataLayer into the contents[] shape Meta Pixel and Pinterest Tag expect.
  • Fix: Pinterest event-name typos in manual setup; correct catalog content_ids parameter.

3.4.1

  • Fix: dataLayer items include item_id alongside id so GA4’s Items report no longer shows “(not set)” for products.

3.4.0

  • Fix: Event log timestamps stable across hosts with mismatched PHP/WordPress timezones (stored UTC, displayed via wp_date()).
  • Fix: GTM template no longer fails import with “Unrecognized value [EVENT]”.
  • New: bot/crawler UA filter before queue insert. Purchase events exempt. Filterable via mcapi_is_bot_request.
  • New: Action Scheduler used for recurring tasks when available — more reliable than WP-Cron on low-traffic sites.

3.3.0

  • New: REST API endpoint /wp-json/mcapi/v1/event for cache-safe browser tracking — no nonce needed (works behind 7-day page caches). Secured by same-origin, per-IP rate limit, body cap, event whitelist.
  • Improvement: reliable retries on transient API failures (5xx, 429, network).
  • Improvement: real client IP via CF-Connecting-IP / X-Forwarded-For / X-Real-IP (sites behind Cloudflare / LB no longer hit rate limits prematurely).
  • Improvement: Safari ITP bypass — _fbp / _fbc cookies rewritten server-side with 90-day TTL.
  • Improvement: phone numbers normalized to E.164 using billing country; external_id SHA-256 hashed; cron lock on queue processor; guest external_id is a cookie-backed UUID.

For older versions (3.2.x and below), see the SVN repository history at https://plugins.svn.wordpress.org/easy-meta-capi/tags/.