GA4 Measurement Protocol Guide: Purchases, Timing Rules, Validation
How the GA4 Measurement Protocol works, when a store should use it, purchase and refund payloads, the timing rules that decide whether attribution survives, validation and DebugView, and when to use the Data Manager API instead.
The GA4 Measurement Protocol is what you reach for when GA4 needs events that client-side tracking can’t capture. We used it to build a Shopify app that recovers purchases missing from GA4, and this article covers the parts that tend to trip people up when they use it for ecommerce.
The first half covers what the Measurement Protocol is, whether you need it, and where it fits for an online store. From “Purchase and refund payloads” on, it’s aimed at whoever implements it. Specs follow Google’s documentation as of October 2026.
What is the GA4 Measurement Protocol?
The GA4 Measurement Protocol (MP) is Google’s API for sending events to GA4 directly from a server. Normally, GA4 events fire when the Google tag runs in the buyer’s browser. With MP, your server sends them without going through the browser.
Google positions MP as a way to augment automatic collection through the Google tag, Tag Manager or Firebase, not to replace it. Its docs say you need tagging to use MP effectively, and that server-only implementations may get only partial reporting.
| Client-side tagging (Google tag) | Measurement Protocol | |
|---|---|---|
| Where events come from | The buyer’s browser | Your server |
| What triggers them | Page views and interactions | Server-side events: orders, refunds, POS sales |
| Session context (source, page, device) | Collected automatically | Added by you; inherited only if you pass the original session’s IDs |
| Affected by ad blockers | Yes | The request isn’t, but the IDs are captured in the browser, so blocked visits may end up with no source / medium |
| Error handling | — | No HTTP error codes, even for malformed payloads |
Universal Analytics had a Measurement Protocol too, but it’s a different API. GA4’s MP uses the /mp/collect endpoint and an API secret.
As of October 2026, Google’s MP docs carry a notice: MP has reached a mature, finalized product state with no plans for deprecation, but new server-to-server integrations should use the Data Manager API. The differences are covered below.
Measurement Protocol vs server-side GTM
Server-side GTM (sGTM) receives events from the browser on your own server and forwards them to GA4 and other destinations. The first signal still has to come from the browser, so it can’t recover a purchase whose purchase event never fired. MP sends what happened on the server (such as an order being created) directly, so it can.
Do you need the Measurement Protocol?
Whether you need it comes down to how many purchases are missing from GA4.
- Check the basics: the GA4 purchase event should fire exactly once per order. Duplicate tags or a purchase event that doesn’t fire on the thank you page are client-side problems. Fix those in your tags, not with MP.
- Size the gap: reconcile Shopify order IDs against GA4 transaction IDs. See GA4 Not Tracking All Shopify Purchases? for the steps.
- Pick an approach: if the gap is too big to ignore, build it yourself (with a developer) or use an app.
If you build it, these are the requirements to hand to your developer:
- Capture the buyer’s client_id and session_id at the time of the order and store them with the order
- Send the order to GA4 with MP from your server when it comes in
- Check whether GA4 already has the order before sending (to avoid double counting)
- Don’t send orders from buyers who declined analytics consent
- Build a way to check that events actually arrive, since MP doesn’t return errors
When ecommerce stores use the Measurement Protocol
An online store uses MP when the order exists on the server but the event never reaches GA4 from the browser.
| Scenario | Event | Tie to the original session? | Watch out for |
|---|---|---|---|
| Purchase event never fired (off-site payment gateways, ad blockers) | purchase | Yes, if the IDs were captured | Sending an order GA4 already has double counts it |
| Refunds | refund | Same transaction_id as the purchase | Recorded on the refund date |
| POS and phone orders | purchase | Only if you can match on user_id | No session, so conversion rate looks higher |
| Recurring subscription orders | purchase | Usually not | Decide first whether these count as ad conversions |
| Agentic and in-app checkouts (e.g., ChatGPT Instant Checkout, TikTok Shop) | purchase | No (there’s no visit) | Set source / medium yourself |
What you need before sending
You need three things to send: a measurement ID, an API secret and a client_id. To join the original session, you also need the session_id.
| What | Where to get it | Example |
|---|---|---|
| Measurement ID (measurement_id) | Web stream details in GA4 | G-XXXXXXXXXX (not the stream ID) |
| API secret (api_secret) | Created in the web stream details | One per stream |
| client_id | The Google tag in the buyer’s browser | 1234567890.1712345678 |
| session_id | Same. Required for session attribution | 1712345678 |
How to create an API secret
- In GA4, open Admin, and under Data collection and modification, click Data streams.
- Select your web stream, and in the Events section of Web stream details, click Measurement Protocol API secrets.
- Click Create, enter a nickname, and click Create.
- Copy the Secret value and store it in your server configuration.
Never put the API secret in your site’s HTML or in client-side code. Google’s docs also say to rotate it regularly to avoid spam. Secrets are case-sensitive, scoped to one stream, and can be revoked.
Purchase and refund payloads
From here on, it gets technical. Send a JSON POST to https://www.google-analytics.com/mp/collect with the measurement ID and API secret in the query string. For EU data collection, use https://region1.google-analytics.com/mp/collect.
Here’s a single Shopify order sent as a purchase. Replace client_id and session_id with the values captured from the original visit.
curl -X POST "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"client_id": "1234567890.1712345678",
"events": [{
"name": "purchase",
"params": {
"transaction_id": "1001",
"currency": "USD",
"value": 89.00,
"tax": 7.12,
"shipping": 0,
"session_id": 1712345678,
"engagement_time_msec": 100,
"items": [{
"item_id": "SKU-001",
"item_name": "Art Print A2",
"price": 89.00,
"quantity": 1
}]
}
}]
}'
- value: in Google’s event reference, value is the sum of price × quantity across items, excluding tax and shipping. If your existing tag sends value including tax, match it so revenue stays consistent.
- Timestamp: this example omits timestamp_micros, so GA4 uses the time it receives the event. To record the order time, add it in microseconds (not milliseconds). It can’t be more than 72 hours old, and for session attribution it must fall within that session.
- Consent: add Consent Mode v2 signals at the top level of the request, for example
"consent": {"ad_user_data": "GRANTED", "ad_personalization": "DENIED"}, using the consent state you captured at checkout. If you omit it, GA4 uses the consent state from the client’s online interactions. There’s no field for analytics consent, so the way to respect an analytics opt-out is not to send the order at all.
For refunds, send a refund event with the same transaction_id as the purchase. A full refund only needs transaction_id and value. For a partial refund, include the refunded items with item_id and quantity.
{
"client_id": "1234567890.1712345678",
"events": [{
"name": "refund",
"params": {
"transaction_id": "1001",
"currency": "USD",
"value": 89.00,
"items": [{ "item_id": "SKU-001", "price": 89.00, "quantity": 1 }]
}
}]
}
Refunds usually happen more than 72 hours after the purchase, so you can’t backdate them and they’re recorded on the refund date. For multiple partial refunds, send one event per refund and make the sender idempotent so the same refund isn’t sent twice.
Main fields
| Field | What it is | Notes |
|---|---|---|
| client_id | Identifies the browser instance | Required. Must match the ID generated by the Google tag on your site |
| user_id | Your logged-in user ID | Optional |
| timestamp_micros | When the event happened, in microseconds | Up to 72 hours back |
| consent | ad_user_data and ad_personalization | Top level of the request |
| user_location, ip_override, device, user_agent | Geo and device information | For events with no visit behind them. Added in 2025 |
| events[].name | Event name | Reserved names such as session_start are rejected |
| params.session_id | Session ID of the original visit | A number. Required for session attribution |
| params.engagement_time_msec | Engagement time since the previous event, in ms | Without it, events may not show up in Realtime or be fully reflected in Realtime user counts and engagement metrics |
non_personalized_ads is deprecated; use consent.ad_personalization instead.
Limits and reserved names
| Limit | Value |
|---|---|
| Events per request | 25 |
| Parameters per event | 25 |
| Custom item parameters | 10 |
| Event and parameter names | 40 characters max; alphanumeric and underscores only; must start with a letter |
| Parameter values | 100 characters (500 for Analytics 360 properties) |
| User properties | 25 per request; names up to 24 characters, values up to 36 |
| Request body | Under 130 KB |
| Volume | 100 million non-conversion requests per hour per property; anything over is silently dropped for the rest of the hour |
Parameter names can’t start with _, firebase_, ga_, google_ or gtag.. Event names such as session_start, first_visit and user_engagement are reserved. You don’t need to send session_start: a new session_id starts a new session.
Measurement Protocol timing rules: 48 and 72 hours
Backdating is capped at 72 hours, and Google Ads export requires sending within 63 days of the latest online event. The deadlines depend on what you need the event for. Here they are, from Google’s use-case table.
| Use case | Deadline | Requires |
|---|---|---|
| Assigning a User-ID | Same business day as the online session (property time zone) | session_id |
| Export to Google Ads and other ad platforms | Within 63 days of the latest online event, and within the key event’s attribution window | GA4 linked to Google Ads |
| Audiences | Within 30 days of the latest online event (42 for apps) | — |
Two rules apply across the board:
- 72 hours: events can be backdated up to 72 hours. With
validation_behaviorunset orRELAXED, older timestamps are accepted but reset to 72 hours ago; withENFORCE_RECOMMENDATIONS, they’re rejected. - 48 hours: events meant to be joined with client-side events (conversion attribution, for example) should be received within 48 hours of the original client-side event timestamp. Later events may not be processed as expected.
Getting the client_id and session_id
To tie an event to the original visit, capture the client_id and session_id generated by the Google tag at the time of the order. If you send a random client_id, GA4 treats the purchase as a new user with no session.
- gtag.js:
gtag('get', 'G-XXXXXXXXXX', 'client_id', callback), and the same withsession_id. - Google Tag Manager: since December 2025, you can send the values of the Analytics Client ID and Analytics Session ID built-in variables as is.
- Cookie values: since February 2026, MP also accepts the raw
_gacookie value for client_id and the_ga_<container-id>cookie value for session_id, although Google still recommends sending non-cookie values.
On Shopify, you can’t add scripts directly to checkout. Custom pixels and app pixels in Settings > Customer events run sandboxed on checkout and the thank you page, so the usual approach is to read the IDs there and pass them to your backend to match with the order.
How to validate Measurement Protocol events (validation server and DebugView)
MP doesn’t return HTTP error codes, even when the payload is malformed or the data isn’t processed. In practice that means a 204 even when nothing shows up in GA4, so validate another way:
- Validation server: send the same payload to
/debug/mp/collectand readvalidationMessages. During development, add"validation_behavior": "ENFORCE_RECOMMENDATIONS"for stricter checks. Events sent to the validation server don’t show up in reports. - Realtime: events sent to the production endpoint typically appear in the Realtime report within a few seconds.
- DebugView: add
"debug_mode": trueto the event params, setengagement_time_msecto a positive value, and send to the production endpoint.
The validation server only checks structure. An empty validationMessages array doesn’t mean your api_secret or measurement ID is right, or that a purchase has transaction_id, items and currency. Send a small volume to production and check the actual data in Realtime or DebugView.
When events don’t show up in DebugView
- debug_mode is missing from the event params (it’s rejected at the top level of the request)
- engagement_time_msec is 0 or missing
- You’re sending to the validation server, whose events never appear in DebugView
- The API secret belongs to a different stream (and you still get a 204)
Adding a custom parameter such as gapfill=1 to server-side events lets you separate them from client-side purchases in an Exploration later.
Double counting and Google Ads
A server-side purchase and a client-side purchase with the same transaction_id won’t necessarily be deduplicated. GA4’s help center says purchases with the same transaction ID from the same user are deduplicated for web streams, but it doesn’t say whether that applies to MP. Deduplication doesn’t work for app streams, and if you send an empty transaction_id, all empty ones are treated as duplicates of each other.
Double counting inflates both GA4 revenue and ad conversions. Before sending, query the GA4 Data API for the transaction_id and only send orders that aren’t there.
If GA4 is linked to Google Ads, key events sent through MP are exported to Google Ads too. If you run Shopify’s Google & YouTube app, Google Ads may have two purchase conversion actions: the app’s own and the one imported from GA4. If both are primary conversion actions, every purchase you recover is counted twice, and Smart Bidding optimizes toward inflated numbers. Check your conversion actions in Google Ads and make one primary and the other secondary.
Meta’s server-side equivalent, the Conversions API, works differently: it’s designed to run alongside the pixel, with deduplication by event_id.
Measurement Protocol vs Data Manager API
If your store only needs to add purchases to GA4 from its own backend, MP is enough. Consider the Data Manager API if you want to send to Google Ads in the same pipeline.
The Data Manager API is Google’s newer API for sending data to Google Ads, GA4 and other products. In May 2026, it added support for sending any non-reserved event to GA4 web and app data streams.
| Measurement Protocol | Data Manager API | |
|---|---|---|
| What you need to start | An API secret | A Google Cloud project and OAuth (user or service account); allowlisting for some features |
| Destinations | One stream per request | Multiple destinations per request (GA4, Google Ads) |
| Data model | GA4-specific | Shared across Google’s ads products |
| Payload encryption (both use HTTPS) | Not supported | Supported |
| Errors | Only reported for requests sent to the validation server | Fast-fail: one bad record fails the whole request |
| Deduplication | Not documented | With allowlisting, deduplicates against the website tag by transaction ID (first instance received wins) |
| Status | Mature, finalized; no deprecation planned | Recommended for new integrations |
The Data Manager API’s deduplication isn’t available to everyone right away: it requires allowlisting, and data sent to a web stream doesn’t feed Google Ads bidding for the first 14 days. And if you send to both GA4 and Google Ads while also exporting GA4 key events to Google Ads, you’ve created a double-counting path on the Ads side.
Common Measurement Protocol mistakes
- Sending a random client_id: GA4 records a new user with no session and no source / medium.
- Using the API secret from a different stream: secrets are per stream, and you still get a 204, so it’s easy to miss.
- Sending timestamp_micros in milliseconds: you’re three digits short and the event lands at the wrong time.
- Leaving out session_id or engagement_time_msec: events may not show up in Realtime or count toward engagement metrics.
- Resending purchases GA4 already has: a matching transaction_id doesn’t guarantee deduplication.
- Hard-coding consent: copying GRANTED from an example onto every order marks data from buyers who never consented as usable for ads.
- Expecting Modify event or Create event rules to run: GA4 doesn’t apply event modification or creation rules to MP events, so set names in your own code.
Using the Measurement Protocol with Shopify
A DIY Shopify setup reads the client_id and session_id with a custom pixel and sends from a server that receives order webhooks. Decide these up front:
- When to send: with cash-on-delivery, bank transfers or buy now, pay later, the order is created before it’s paid. Sending on orders/create versus orders/paid changes what “revenue” means in GA4.
- Duplicate deliveries: Shopify can deliver the same webhook more than once and doesn’t guarantee order. Deduplicate on the webhook ID.
- Missed webhooks: run a reconciliation job against the orders list to catch anything that never arrived.
- Customer data: apps that subscribe to order webhooks need approval for Shopify’s protected customer data.
Per Shopify’s docs, checkout_completed usually fires on the thank you page, or on the first upsell page if there is one, and doesn’t fire if that page never loads. That’s where missing purchases come from.
We built all of that into a Shopify app, GapFill. It’s our own product, so read with that in mind. It recovers only the purchases missing from GA4, sending them through MP with the IDs from the original visit. It works alongside your existing tags, and the missing purchases report is free. For how it works and how it compares with other approaches, see GA4 Not Tracking All Shopify Purchases?
Our Shopify appGapFill: Recover missing GA4 purchase events server-sideWorks alongside your existing tags. Free missing purchases report.See GapFill →Sources
- Measurement Protocol (Google for Developers)
- Send Measurement Protocol events to Google Analytics (Google for Developers)
- Measurement Protocol reference (Google for Developers)
- Measurement Protocol events reference (Google for Developers)
- Validate events (Google for Developers)
- Verify implementation (Google for Developers)
- Measurement protocol use cases (Google for Developers)
- Troubleshooting (Google for Developers)
- Measurement Protocol changelog (Google for Developers)
- Upgrade from Measurement Protocol to Data Manager API (Google for Developers)
- Send events to Google Analytics with Data Manager API (Google for Developers)
- Data Manager API: Introducing support for Google Ads store sales and more event ingestion use cases for Google Analytics (Google Ads Developer Blog, May 2026)
- [GA4] Minimize duplicate key events with transaction IDs (Google Analytics Help)
- checkout_completed (Shopify.dev)
- Webhooks (Shopify.dev)
FAQ
What is the GA4 Measurement Protocol?
It's Google's API for sending events to GA4 from a server over HTTP, authenticated with a measurement ID and an API secret. Because it bypasses the browser, it can record events client-side tracking misses, such as purchases where the purchase event never fired, refunds, and offline orders.
Is the Measurement Protocol free?
Google doesn't charge for it. Standard GA4 properties are limited to 100 million non-conversion requests per hour per property. You still pay for whatever server and development work sends the events.
Is the Measurement Protocol being deprecated?
No. As of October 2026, Google's docs say MP has reached a mature, finalized product state with no plans for deprecation, while recommending the Data Manager API for new server-to-server integrations.
Will server-side events inflate my sessions or users?
Not if you send the client_id and session_id from the original visit: the events join that session. Without a session_id, events start a new session; with a made-up client_id, GA4 counts a new user.
Why don't my Measurement Protocol events show up in DebugView?
To appear in DebugView, events need debug_mode in their params and a positive engagement_time_msec, and they have to go to the production endpoint. Events sent to the validation server (/debug/mp/collect) never appear in DebugView or reports.
Will server-side purchases keep their source / medium?
Yes, if you send the client_id and session_id from the original session. If you override timestamp_micros, keep it within that session. Without those IDs, session-scoped reports may show (not set).
- MP sends events to GA4 from your server. It augments client-side tagging rather than replacing it.
- Ecommerce use cases: purchases where the purchase event never fired, refunds, POS and phone orders, and recurring subscription orders.
- To keep session attribution, capture the client_id and session_id generated by the Google tag and send them with the event.
- Backdating is capped at 72 hours, events meant to be joined with client-side data should arrive within 48 hours, and Google Ads export requires sending within 63 days.
- MP doesn't return HTTP error codes, even for malformed payloads. The validation server only checks structure, so check Realtime or DebugView too.
- A matching transaction_id doesn't guarantee deduplication. Check GA4 for the order before sending.
- Google recommends the Data Manager API for new integrations, but MP isn't being deprecated.
Written byRyushi Nagata
CEO, ULTRO Inc. (Tokyo)Shopify Partner. Ryushi handles Shopify store builds, maintenance, advertising and CRM end to end, and has managed more than ¥4 billion in ad spend over his career. He holds Google Ads certifications (Search, Display, Video, Shopping, Apps, Performance Max, Measurement) and the Google Analytics certification.
How many purchases is GA4 missing in your store?
GapFill matches your Shopify orders against GA4 purchases, shows what GA4 missed, and recovers those purchase events server-side. It works alongside your existing tags, and the missing purchases report is free.