GA4 Measurement Protocol v2 reference

last verified · against GA4 Measurement Protocol as of 2026-07

Complete GA4 Measurement Protocol reference: endpoints, JSON body fields, reserved names, limits, validation server, consent, and user-provided data.

What this is

The GA4 Measurement Protocol sends events to Google Analytics over plain HTTP POST — from a server, a CRM, a point-of-sale, anywhere without gtag. Google is explicit about its scope: “The intent of the Measurement Protocol is to augment automatic collection through gtag, Tag Manager, and Google Analytics for Firebase, not to replace it,” and “While it’s possible to send events to Google Analytics solely with the Measurement Protocol, only partial reporting may be available.”

Endpoints

Purpose URL
Production https://www.google-analytics.com/mp/collect
Validation https://www.google-analytics.com/debug/mp/collect
EU data residency Same paths on https://region1.google-analytics.com

Query parameters (both required):

Stream type Parameter 1 Parameter 2
Web (gtag/GTM) measurement_id (G-…) api_secret
App (Firebase) firebase_app_id api_secret

Create the secret under Admin → Data streams → select stream → Measurement Protocol API secrets → Create. It is private: “Don’t expose it in the client-side code of your website or app.”

Request body reference

Key Type Required Notes
client_id string Yes (web) Web-client instance ID; app streams send app_instance_id instead
events[] array Yes Up to 25 events per request
user_id string No Cross-platform user identifier, UTF-8
timestamp_micros number No Unix microseconds; “Events can be backdated up to 72 hours”
user_properties object No Up to 25 per request; shape { name: { value } }, optional per-property timestamp_micros
user_data object No User-provided data — see below
consent object No ad_user_data and ad_personalization, each GRANTED or DENIED
non_personalized_ads boolean No Deprecated — use consent.ad_personalization
user_location object No city, region_id, country_id, …; takes precedence over ip_override
ip_override string No IP for geo derivation; ignored when user_location is present
device object No Structured device data; user_agent is ignored when device is set
user_agent string No UA string for device derivation
validation_behavior string No RELAXED (default) or ENFORCE_RECOMMENDATIONS

Timestamp precedence: property/event-level timestamp_micros, then request-level, then server receipt time — “Measurement Protocol uses the first timestamp it finds.”

Each event object: name (required, ≤40 characters) and params (≤25 per event). Two parameters matter more than the docs make obvious: session_id (“Required for several common use cases”) and engagement_time_msec. GA joins the latest known geo/device data to MP events unless you send session_id within 24 hours of session start, which joins that session’s data instead. Sending a new session_id starts a session without needing the reserved session_start event.

Limits

Limit Value
Events per request 25
Parameters per event 25
POST body size < 130 kB
Event / parameter name length 40 characters
Parameter value length 100 characters (500 on GA 360)
User properties per request 25
User property name / value 24 / 36 characters
Backdating window 72 hours
Items array custom parameters 27 per item, beyond prescribed item params

Reserved names

Event names you cannot send (mostly auto-collected):

reserved-event-names.txt
ad_activeview ad_click ad_exposure ad_query ad_reward
adunit_exposure app_clear_data app_exception app_install
app_remove app_store_refund app_update app_upgrade
dynamic_link_app_open dynamic_link_app_update dynamic_link_first_open
error firebase_campaign firebase_in_app_message_action
firebase_in_app_message_dismiss firebase_in_app_message_impression
first_open first_visit notification_dismiss notification_foreground
notification_open notification_receive notification_send
os_update session_start user_engagement

ad_impression, in_app_purchase, and screen_view “are only allowed for App streams.”

Scope Reserved prefixes
Parameter names _ (underscore), firebase_, ga_, google_, gtag.
User property names _ (underscore), firebase_, ga_, google_

Reserved user property names: first_open_time, first_visit_time, last_deep_link_referrer, user_id, first_open_after_install.

User-provided data (user_data)

Unlike gtag, which hashes automatically, MP requires you to SHA-256 hash and hex-encode sensitive fields yourself before sending: sha256_email_address, sha256_phone_number, and inside address[]: sha256_first_name, sha256_last_name, sha256_street (plain: city, region, postal_code, country). Up to 3 emails/phones and 2 addresses as arrays; a single value may be passed without the array.

Normalization before hashing: trim whitespace, lowercase; emails drop periods before the domain for gmail.com/googlemail.com; phone numbers strip non-digits and use E.164 with a + prefix; names drop digits and symbols; street addresses drop symbols.

Validation server

POST the identical payload to /debug/mp/collect. Response:

validation-response.json
{
"validationMessages": [
{ "fieldPath": "events", "description": "…", "validationCode": "NAME_INVALID" }
]
}

Empty validationMessages = pass. Codes: NAME_INVALID, VALUE_INVALID, VALUE_REQUIRED, NAME_RESERVED, VALUE_OUT_OF_BOUNDS, EXCEEDED_MAX_ENTITIES, NAME_DUPLICATED. The caveat that bites: “The validation server does not validate the api_secret or measurement_id” — a passing payload can still land nowhere.

Gotchas

  1. Events validate but never appear in reports. Cause: wrong measurement_id or api_secret — the validation server checks neither. Fix: verify both against the stream’s Measurement Protocol API secrets page, then watch Realtime.
  2. MP events carry stale geography or device data. Cause: without session_id sent within 24 hours of session start, GA joins the most recent geo/device info for that identifier. Fix: pass the live session_id from the browser context (session state is persisted in the _ga_<container-id> cookie) or set user_location/device explicitly.
  3. A backfill silently loses events. Cause: timestamp_micros older than 72 hours, or milliseconds sent where microseconds are required. Fix: chunk backfills inside the window and multiply epoch-milliseconds by 1,000.
  4. An event named like an auto-collected one is rejected. Cause: the reserved list above (e.g. session_start, first_visit). Fix: rename; for sessions, minting a new session_id is the supported route.
  5. Audience triggers and event edits don’t fire. Cause: “Rules for generating or renaming events aren’t triggered by events sent with the Measurement Protocol.” Fix: send the final event shape from the server — don’t rely on GA4’s modify/create event features.
  6. A pure server-side property reports oddly. Cause: MP alone yields “only partial reporting”; Google requires tagging alongside it. Fix: keep gtag/GTM (or sGTM) as the primary collector and treat MP as augmentation.

Quick recipes

Minimal event with session attribution:

mp-send.sh
curl -s "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=SECRET" \
-H "Content-Type: application/json" \
-d '{
"client_id": "123456.7654321",
"events": [{
"name": "refund_issued",
"params": { "session_id": "1721470000", "engagement_time_msec": 100 }
}]
}'

Purchase with items (required: currency, value, transaction_id, items):

mp-purchase.json
{
"client_id": "123456.7654321",
"events": [{
"name": "purchase",
"params": {
"currency": "USD", "value": 43.5, "transaction_id": "T-10001",
"items": [{ "item_id": "SKU_1", "item_name": "Sticker pack", "price": 43.5, "quantity": 1 }]
}
}]
}

Validate before shipping:

mp-validate.sh
curl -s "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXX&api_secret=SECRET" \
-H "Content-Type: application/json" -d @payload.json
# expect: {"validationMessages":[]}

Consent state plus pre-hashed user data:

mp-consent-userdata.json
{
"client_id": "123456.7654321",
"consent": { "ad_user_data": "GRANTED", "ad_personalization": "DENIED" },
"user_data": { "sha256_email_address": ["<sha256-hex of normalized email>"] },
"events": [{ "name": "crm_signup", "params": { "session_id": "1721470000" } }]
}

Sources

Official pages these values were read from (2026-07-19):

Two things the docs do not state, flagged rather than guessed: no request rate limit is published for the MP endpoint, and the docs do not specify which reports MP-only data is excluded from beyond “only partial reporting may be available.”

Changelog

  • — Initial version, verified against official Google developer documentation.

dataLayer

0 events · 0 sent

    • home /
      writing /writing
      guides /guides
      work /#work
      about /about
      colophon /colophon
      toggle analyst mode ctrl+.
      print session receipt /#receipt