ULTRO NOTES Published Updated

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.

In shortThe GA4 Measurement Protocol (MP) lets you send events to GA4 directly from your server. It's meant to augment client-side tagging, not replace it: use it for events the browser can't capture, such as purchases where the purchase event never fired, refunds, and offline or POS orders. To keep session attribution, send the client_id and session_id generated by the Google tag with the event. Backdating is capped at 72 hours. Google now recommends the Data Manager API for new server-to-server integrations, but MP has no deprecation plans.

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 fromThe buyer’s browserYour server
What triggers themPage views and interactionsServer-side events: orders, refunds, POS sales
Session context (source, page, device)Collected automaticallyAdded by you; inherited only if you pass the original session’s IDs
Affected by ad blockersYesThe 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.

  1. 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.
  2. Size the gap: reconcile Shopify order IDs against GA4 transaction IDs. See GA4 Not Tracking All Shopify Purchases? for the steps.
  3. 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.

ScenarioEventTie to the original session?Watch out for
Purchase event never fired (off-site payment gateways, ad blockers)purchaseYes, if the IDs were capturedSending an order GA4 already has double counts it
RefundsrefundSame transaction_id as the purchaseRecorded on the refund date
POS and phone orderspurchaseOnly if you can match on user_idNo session, so conversion rate looks higher
Recurring subscription orderspurchaseUsually notDecide first whether these count as ad conversions
Agentic and in-app checkouts (e.g., ChatGPT Instant Checkout, TikTok Shop)purchaseNo (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.

WhatWhere to get itExample
Measurement ID (measurement_id)Web stream details in GA4G-XXXXXXXXXX (not the stream ID)
API secret (api_secret)Created in the web stream detailsOne per stream
client_idThe Google tag in the buyer’s browser1234567890.1712345678
session_idSame. Required for session attribution1712345678

How to create an API secret

  1. In GA4, open Admin, and under Data collection and modification, click Data streams.
  2. Select your web stream, and in the Events section of Web stream details, click Measurement Protocol API secrets.
  3. Click Create, enter a nickname, and click Create.
  4. 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

FieldWhat it isNotes
client_idIdentifies the browser instanceRequired. Must match the ID generated by the Google tag on your site
user_idYour logged-in user IDOptional
timestamp_microsWhen the event happened, in microsecondsUp to 72 hours back
consentad_user_data and ad_personalizationTop level of the request
user_location, ip_override, device, user_agentGeo and device informationFor events with no visit behind them. Added in 2025
events[].nameEvent nameReserved names such as session_start are rejected
params.session_idSession ID of the original visitA number. Required for session attribution
params.engagement_time_msecEngagement time since the previous event, in msWithout 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

LimitValue
Events per request25
Parameters per event25
Custom item parameters10
Event and parameter names40 characters max; alphanumeric and underscores only; must start with a letter
Parameter values100 characters (500 for Analytics 360 properties)
User properties25 per request; names up to 24 characters, values up to 36
Request bodyUnder 130 KB
Volume100 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 caseDeadlineRequires
Assigning a User-IDSame business day as the online session (property time zone)session_id
Export to Google Ads and other ad platformsWithin 63 days of the latest online event, and within the key event’s attribution windowGA4 linked to Google Ads
AudiencesWithin 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_behavior unset or RELAXED, older timestamps are accepted but reset to 72 hours ago; with ENFORCE_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 with session_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 _ga cookie 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:

  1. Validation server: send the same payload to /debug/mp/collect and read validationMessages. During development, add "validation_behavior": "ENFORCE_RECOMMENDATIONS" for stricter checks. Events sent to the validation server don’t show up in reports.
  2. Realtime: events sent to the production endpoint typically appear in the Realtime report within a few seconds.
  3. DebugView: add "debug_mode": true to the event params, set engagement_time_msec to 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 ProtocolData Manager API
What you need to startAn API secretA Google Cloud project and OAuth (user or service account); allowlisting for some features
DestinationsOne stream per requestMultiple destinations per request (GA4, Google Ads)
Data modelGA4-specificShared across Google’s ads products
Payload encryption (both use HTTPS)Not supportedSupported
ErrorsOnly reported for requests sent to the validation serverFast-fail: one bad record fails the whole request
DeduplicationNot documentedWith allowlisting, deduplicates against the website tag by transaction ID (first instance received wins)
StatusMature, finalized; no deprecation plannedRecommended 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

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

Key takeaways
  • 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.

See GapFill →