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):
ad_activeview ad_click ad_exposure ad_query ad_rewardadunit_exposure app_clear_data app_exception app_installapp_remove app_store_refund app_update app_upgradedynamic_link_app_open dynamic_link_app_update dynamic_link_first_openerror firebase_campaign firebase_in_app_message_actionfirebase_in_app_message_dismiss firebase_in_app_message_impressionfirst_open first_visit notification_dismiss notification_foregroundnotification_open notification_receive notification_sendos_update session_start user_engagementad_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:
{ "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
- Events validate but never appear in reports. Cause: wrong
measurement_idorapi_secret— the validation server checks neither. Fix: verify both against the stream’s Measurement Protocol API secrets page, then watch Realtime. - MP events carry stale geography or device data. Cause: without
session_idsent within 24 hours of session start, GA joins the most recent geo/device info for that identifier. Fix: pass the livesession_idfrom the browser context (session state is persisted in the_ga_<container-id>cookie) or setuser_location/deviceexplicitly. - A backfill silently loses events. Cause:
timestamp_microsolder than 72 hours, or milliseconds sent where microseconds are required. Fix: chunk backfills inside the window and multiply epoch-milliseconds by 1,000. - 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 newsession_idis the supported route. - 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.
- 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:
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):
{ "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:
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:
{ "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" } }]}Related
Sources
Official pages these values were read from (2026-07-19):
- Measurement Protocol overview
- Sending events
- JSON body reference
- Events reference
- User properties
- Validating events
- User-provided data via MP
- MP, SDK, and User ID feature policy
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.