Webhook Matching
Ensure AdPage's webhook functionality always sends purchase events towards GA4 and marketing platforms with the correct consent signals and marketing data
What does Webhook Matching do?
Purchase events that fire in the browser are fragile. Ad blockers can stop them, cookie restrictions can strip them, and Safari's ITP can cut the data short before it ever reaches you. Server-side tagging fixes these issues, but there are still three scenarios where the browser-side purchase tracking won't work.
Visitors who never come back to your thank-you page. They pay in their banking app, see the confirmation there, and close it. Job done in their eyes. If they don't land on your thank-you page, the browser event never fires.
Visitors who do land on the thank-you page but leave too quickly. Your tracking scripts need a moment to load and send. Close the tab before that and the event tracking dies with it.
Someone starts their journey in an in-app browser, maybe inside an LLM app or a social app, then gets sent off to their payment app. On the way back, the payment app hands them over to whatever browser their phone has set as default. Now they're in a totally different browser with a different session and no cookies from the original visit, so even a perfectly loaded thank-you page reports it as something other than what it is.
Backend webhooks from your shop platform (Shopify, WooCommerce, Magento) don't have that problem because they get sent straight from the server. The trade-off is that they can show up without any browser context, so no gclid, no user-agent, and no items array. AdPage's Webhook Matching links the browser data and the webhook data together using a shared match identifier, merges the payloads, and forwards a single validated event downstream. The result: no duplicate conversions, and no missed ones either.


Setting up Webhook Matching
There are two steps to setting up Webhook Matching.
Step 1 is processing the incoming webhook (Trigger) and a begin_checkout event (Prepare) to ensure that those can be matched.
After the Matching rate is at a high enough percentage (>90%) these matched webhooks can be sent towards Google Analytics and your marketingplatforms, setting that up is Step 2.
Step 1: Setting up the Webhook Matching Rate
Open the server container in Google Tag Manager
Go to Templates → Tag Templates → New
Click the three dots in the top right → Import → select
adpage-event-notifier.tpl. You can download it here: https://adpage.b-cdn.net/GTM-Templates/adpage-event-notifier.tplClick Save. The "AdPage Event Notifier" template now appears in the templates list
Create two tags using this template:
Tag 1 — Prepare (event received from client-side):
Mode:
PrepareContainer ID: found in the URL of your AdPage server container
Platform preset: choose whichever applies (see below)
Event name:
purchaseTrigger: a browser event just before completing the checkout (e.g.
begin_checkout)Give it a correct name: "AdPage Event Notifier - Prepare"

Tag 2 — Trigger (server-side webhook):
Mode:
TriggerContainer ID: found in the URL of your AdPage server container
Platform preset: same as above (see below)
Transaction ID: an Event Data variable with
transaction_idas the Key PathTrigger: the webhook event from your commerce platform (the webhook that gets processed by the AdPage Webhook Client)
Give it a "AdPage Event Notifier - trigger"

Platform Preset
The platform preset option is the BasketKey you want to use to link the Prepare and Trigger sides together, both must send the same value, or they won't match.
Client has the AdPage Tagging plugin/script installed
AdPage Tagging (universal) ⭐
user_id (from the trytagging_user_id cookie)
Native Shopify (no Tagging plugin)
Shopify
cart_token
Native Magento
Magento
quote_id
Native Lightspeed C-Series
Lightspeed
quote_id
Native Shopware 6
Shopware
cart_token
Something else / custom integration
Custom
define your own
Step 1 result would be:
Publish the Google Tag Manager server container with these 2 new tags. The incoming begin_checkout events and webhooks will be automatically matched in your AdPage server container. Check back in a couple of days to check on the Webhook Matching Rate.
Step 2: Validate the webhook matching rate
A couple of days after publishing step 1 you are able to check the webhook matching rate within your AdPage server container.

If the match rate is above 90% you can continue with Step 3 below.
If the match rate is below 90% you should alter the webhook matching configuration. Alter the configuration in the top right behind the Configuration button. Here you'll find the following configuration settings:
Extra identifier (fallback)
Pick a field present in both prepare and trigger (user_id, cart_token). It's the only deterministic option, takes precedence over the others, and keeps the normal Matched status. Usually the biggest single gain.
Transaction ID prefix Only needed when the same order arrives via two sources with different ID formats. Fill in the prefix and tick the "ignore" checkbox so the bare duplicate is dropped. Leave empty if there's one source.
Probabilistic matching Off by default. Links missed orders to a recent prepare based on items, value, identity and timing. Status becomes Probable. Enable when no fallback field is available; be careful on shops with many near-identical low-value orders.
Trigger-only
Off by default. Sends the order on the _ga client_id alone, without a prepare. Recovers orders where the prepare never fired. Weaker attribution, but the conversion is preserved. Status: Trigger-only.
Unconsented conversions On by default, leave it on. Sends a cookieless, PII-free ping for denied-consent orders so GA4 can model them. Only switch off after checking the client's privacy policy.
Step 3: Utilizing the matched Webhooks
To be able to utilize the matched webhooks, you'll have to create a GA4 Measurement Protocol client. The Webhook Matching callback arrives on your sGTM /mp endpoint. A client needs to be in place there to claim the incoming Measurement Protocol request and convert it into an event that your downstream tags (GA4, Google Ads, Meta CAPI, etc.) can pick up.
Create the client: In your server container, go to Clients → New, choose client type Measurement Protocol (GA4), and give it a recognizable name such as "AdPage MP" Set Path / Activation to your callback URL,
/mp.

Block the purchase events in your regular GA4 trigger: add the condition to your regular GA4 trigger that 'Event Name' does not equal 'purchase'.

Add a new Measurement Protocol Client trigger: create a new trigger that fires on all incoming requests in your Measurement Protocol Client. After creating this trigger, add it to your Google Analytics 4 tag.


Testing and debugging
Before publishing the changes made for Step 2, confirm that Prepare and Trigger are actually matching.
Use the Webhook Replay functionality to test the matched webhooks
To test the matched webhook without placing a new order,
Go to your AdPage container's Webhook Logs
Find a Matched Webhook and open it by clicking on it
Find the Replay button at the bottom

Go to your Google Tag Manager server container and start the preview mode
Click on the 3 dots in the top right and select the option Send requests manually, copy the X-Gtm-Server-Preview HTTP header.
Paste the X-Gtm-Server-Preview HTTP header in the Matched Webhook replay input field
The webhook replays in the Google Tag Manager preview mode so you can see exactly which tags fire and what the outgoing payloads of these tags are.

Reading the Webhook Matching dashboard
Under Webhook Matching → Match health, you can see at a glance how well Prepare and Trigger have matched over the selected period (7 days by default), and where things are going wrong.
The percentage at the top is the share of orders processed successfully — matched, handled via in-app browsers, or cookieless — against the total number of incoming orders.
Matched — Prepare and Trigger were linked on the Basket ID. This is the healthy path.
In-app browsers — orders from in-app browsers (Instagram/Facebook app, etc.) where cookies work in a limited way.
Cookieless — orders with no trackable cookie (see "Sending unconsented conversions" under Advanced options below).
Not reached — orders that weren't matched. Hover over this segment for a breakdown by cause: stale cookies (the Prepare cookie had already expired by the time the Trigger arrived — too much time between checkout start and order confirmation), or other/unclear (a catch-all, usually incidental).
Below the bar you'll see the number of orders without consent — orders that are (partly) not tracked in GA4 because the visitor declined tracking.
Use Analyze missed to run an analysis on "not reached" orders and see whether probabilistic matching or trigger-only forwarding (see Advanced options) could have recovered them.
Click Details on any row under Recent events to inspect an individual event: status, source (transaction ID), and callback time appear at the top.
The payload check flags data mismatches, such as "value ≠ sum of items" when the order total doesn't match the sum of line items within tax/shipping tolerance — this is a warning, not a block; the event still goes through, but it's worth checking whether the client's integration is sending the right fields.
Tracking consent shows whether consent was granted or denied, along with the underlying Consent Mode flags (
analytics_storage,ad_storage,gcs).Browser shows the user-agent, useful for spotting patterns (e.g. a lot of "not reached" events from one browser/OS combination).
Prepare buffered and Trigger processed show the exact timestamps of both events — the gap between them matters for stale-cookie issues. Further down you'll find four tabs — outgoing payload, Prepare data, order data, and raw Trigger body — useful for seeing exactly what came in before processing, plus options to copy the payload or replay the event.

What's in the outgoing payload
The Event Notifier's callback to sGTM carries a complete payload, ready to use across all major ad platforms. The template automatically forwards:
GA4 (Measurement Protocol)
Meta CAPI (Facebook + Instagram)
How deduplication works
⚠️ Do not use
user_idas theevent_id. It remains stable per customer, not per order. A second purchase by the same customer within 48 hours would be incorrectly discarded as a duplicate. Useuser_idas the BasketKey andexternal_id, not as the deduplication key.
⚠️ The
event_idmust be exactly the same string in both events. Watch for prefixes or formatting differences. If the browser sends12345and the server sendsorder_12345, Meta sees two separate events and does not deduplicate them.
💡 Ensure the CAPI
event_idcomes from the Trigger/order side (transaction_id). Do not use a randomevent_idgenerated during the Prepare/begin_checkoutstep. It never matches the browserPurchaseevent.
Source
event_id
Browser-side
Meta Pixel Purchase thankyou page
transaction_id
Server-side
AdPage - Meta Conversion API on the Measurement Protocol Client
transaction_id from payload
Google Ads (conversion + remarketing)
TikTok Events API
Pinterest Conversions API
Consent, across every platform
Troubleshooting
"Match identifier value could not be resolved" — The BasketKey wasn't found in the event data. Check the error log in the sGTM console, find where the UUID actually lives in the EventDataSnapshot, and set that path via Identifier value override in the tag config.
Trigger returns a 308 redirect — The URL ends with
/trigger/and no value. Same root cause as above — recheck BasketKey resolution.Trigger returns a 400 — Body validation failed, usually because
transaction_idorvalueis empty. With the AdPage Tagging plugin (WooCommerce/PrestaShop) this lives atecommerce.transaction_id— confirm the webhook is sending it.missedstatus in Event Notifier logs — A Trigger arrived with no matching Prepare. The browser-side Prepare likely failed (ad blocker, ITP, or no browser event right before checkout) — check sGTM preview on the storefront.callback_failed— sGTM isn't receiving the Event Notifier's callback. Check the callback URL in AdPage's tenant config — usually a missing/datasuffix or the wrong subdomain.
Advanced options
Multi-market setups (Shopify Markets or multi-locale shops) — enter all GA4 property IDs comma-separated in the GA4 Measurement IDs field (e.g.
G-NL123, G-BE456, G-DE789). Event Notifier then forwards the matching_ga_<MID>cookie per property so sGTM can route downstream.WooCommerce without the Tagging plugin — enter the exact
wp_woocommerce_session_<hash>cookie name in the WooCommerce session cookie field.Custom event identifiers — choose the Custom preset and define your own identifier name and variable reference.
Duplicate-identifier prevention — if an order arrives with both a canonical identifier (e.g.
vtnl-...) and a bare variant (e.g. just the order number), the bare variant is ignored and only the canonical one proceeds.Probabilistic matching — attempts to link an otherwise-missed order to a recent Prepare event based on items, amount, customer identity, and timing. When confidence is high enough, the event is sent to GA4 with status Probable instead of Matched. Off by default — only enable this if you trust it for a given client.
Trigger-only forwarding — sends an otherwise-missed order (no Prepare, not even via probabilistic matching) to GA4 anyway, using the GA4
client_idfrom the Trigger's owngacookie. Only works if that client ID is present; otherwise the order stays missed. Status becomes Trigger-only. Off by default.Sending unconsented conversions — forwards an order where tracking consent was declined (so no GA4 client ID) as a privacy-safe, cookieless ping: a disposable client ID with "denied" consent flags and no PII, so GA4 can still model the conversion. Status becomes Unconsented. Off by default — only enable this after discussing consent implications with the client, since it forwards data even without tracking consent.
Need help?
If a client's setup doesn't fit anything covered here, ask our support at support@adpage.io with the container ID and a screenshot of the sGTM console log.
Last updated
Was this helpful?

