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-addressessource (folderapple_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
Purchasebut withcustom_data.customer_status = "subscription_renewal"so you can filter them in Events Manager. - Subscribe / SubscriptionRenewal events — Meta’s standard
Subscribefor sign-ups + aSubscriptionRenewalcustom event for renewals.Purchasestays 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)
- Install and activate the plugin. WooCommerce must already be active.
- 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.
- (If you use GTM) Download the bundled
gtm-template.jsonfrom 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:
- In Meta Events Manager your Pixel Settings Event Setup, turn off “Track Events Automatically Without Code”. This plugin handles all event sending.
- 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 viadata-cfasync="false". -
Does it work with a block-based theme (Twenty Twenty-Five etc.)?
-
Yes.
-
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 themcapi_inline_script_attrsfilter — 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
Purchaseclean. See WooCommerce Subscriptions in Advanced Configuration. -
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 itsevent_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/_gacookies 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
Contribuidores e desenvolvedores
“CAPI Suite: Meta, Pinterest, TikTok, GTM” é um software com código aberto. As seguintes pessoas contribuíram para este plugin.
ContribuidoresTraduzir “CAPI Suite: Meta, Pinterest, TikTok, GTM” para o seu idioma.
Interessado no desenvolvimento?
Navegue pelo código, dê uma olhada no repositório SVN ou assine o registro de desenvolvimento via RSS.
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 (CompletePaymentPurchase,SubmitForm
Lead); the server-side CAPI call now sends the current name, but the bundled GTM
template’s TikTok Purchase tag still sent the retiredCompletePaymentname until this
release. TikTok deduplicates browser and server events by event name plusevent_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-downloadgtm-template.jsonfrom 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 toPurchasemanually (seedocs/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 anyevent_timeolder 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 readsX-Bot-Score(Cloudflare’s documented example name) as well as
Cf-Bot-Score, filterable viamcapi_cf_bot_score_headers, and the readme no longer claims
Enterprise plans send it automatically. - Fix: a malformed
mcapi_cf_bot_score_thresholdfilter 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 became0— 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 thecurrency_minor_unitthe 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 rangebundled/custom. Both read options that stopped being
written in 3.6.0. - Fix:
mcapi_update_apple_relay_ipssurvived 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
PurchaseandLead, replacing the legacy
CompletePayment andSubmitFormthat TikTok renamed in 2025.ClickButtonand
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 yorkinstead ofnewyork), postcodes kept spaces and
dashes (sw1a 1aainstead ofsw1a1aa), US ZIP+4 was sent in full instead of the
leading five digits, and names kept punctuation (o'brieninstead ofobrien).
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
nottclidand 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-Fortrusted 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 toREMOTE_ADDRif 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 existingmcapi_force_global_rate_limitfilter still lets
a merchant turn it off, andmcapi_global_rate_limit_per_minutestill 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_idfallbacks used bareuniqid(), which has microsecond resolution. Two
events generated by concurrent PHP workers in the same microsecond — plausible on a busy
store — could get the sameevent_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-basedsubstr(), which can split a
multibyte UTF-8 character in half. Now truncated withmb_substr()where available. - New:
mcapi_trusted_proxy_hopsfilter 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_queueAction 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 28delete_transient()calls per event per platform on bloatedwp_optionstables — 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 viamcapi_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_bmcookie 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 readsX-Bot-Score(Cloudflare’s documented example header name) orCf-Bot-Score. Sites that haven’t configured it are unaffected: the__cf_bmcookie 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 existingisTrustedgate. Patchable by hostile bot authors (stealthplugins exist), but each bypass is real engineering cost — same cost-moat principle as the rest of the layered detection. human_score >= 100strong-trust now used. Themcapi_ip_state.human_scorecolumn 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_cartevent 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 thefragmentsargument WC passes to theadded_to_cartevent — works regardless of DOM state. Adds a sharedwindow.__mcapi_atc_pusheddedup 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_gapfilter, 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_agentinstead ofbot. 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 themcapi_ai_agent_user_agentsfilter. - “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 (matchesmcapi_ip_state.ip_hash) lets the Event Log JOIN against the behavioral-state table for real-time customer / flagged classification. Legacy rows haveip_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, andwbraidfrom ad-click URLs into 1st-party cookies (_mcapi_gclidetc.) at landing; the Conversion Linker transfers these to_gcl_awfor 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) — seedocs/GTM-MANUAL-SETUP.txtfor full setup. - Pinterest EMQ improvements. Now captures both Pinterest’s persistent
_epikcookie (set by Pinterest’s tag.js on real visitors) and theepikURL parameter. Improves Event Match Quality on Pinterest tag installs running the current (2024+) version, where the legacypina_idflow is being phased out. - Stronger real-user signals in datacenter bypass. Beyond
_fbpand_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.jsonfrom 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_totargets 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_cartevents 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 passfalseto 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/_fbccookie domain strips leadingwww.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_idsparameter.
3.4.1
- Fix: dataLayer items include
item_idalongsideidso 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/eventfor 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/_fbccookies rewritten server-side with 90-day TTL. - Improvement: phone numbers normalized to E.164 using billing country;
external_idSHA-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/.
