GA4 offline conversions via Measurement Protocol
last verified · against GA4 Measurement Protocol offline-conversion docs as of 2026-07
GA4 offline conversions via the Measurement Protocol: client_id and session_id stitching, transaction_id dedup, 72-hour backdate window, consent, and validation.
What this is
The GA4 Measurement Protocol records offline conversions by sending events directly to Google Analytics servers over HTTP, stitching a later server-side, CRM, or point-of-sale interaction back to the browser session that produced it. Recording offline conversions is a named primary use case; the protocol augments automatic gtag, Tag Manager, and Firebase collection rather than replacing it, and a server-to-server-only setup yields only partial reporting.
Endpoints
| Purpose | URL |
|---|---|
| Production (web) | https://www.google-analytics.com/mp/collect |
| Production (EU data) | https://region1.google-analytics.com/mp/collect |
| Validation / debug | https://www.google-analytics.com/debug/mp/collect (EU: region1 host) |
Events posted to /debug/mp/collect do not appear in reports.
Query parameters (both required)
| Parameter | Required | Source |
|---|---|---|
measurement_id |
Yes | The web data stream’s Measurement ID (starts with G-) |
api_secret |
Yes | Admin → Data streams → Measurement Protocol API secrets |
Request body — top-level fields
The offline conversion is a POST body of JSON. consent is request-scoped and
sits alongside client_id and events.
| Field | Scope | Required | Notes |
|---|---|---|---|
client_id |
request | Yes (web) | Joins the offline event to an existing web client/user. Accepts two positive numbers joined by a period (.), or a client-ID cookie value |
events[] |
request | Yes | Up to 25 events per request |
timestamp_micros |
request or event | No | Unix timestamp in microseconds, not milliseconds |
consent |
request | No | ad_user_data and ad_personalization |
validation_behavior |
request | No | RELAXED (default) or ENFORCE_RECOMMENDATIONS |
Event params — offline purchase
Ecommerce fields and session-join fields both travel inside events[].params.
| Param | Purpose |
|---|---|
session_id |
Joins the event to a specific existing session; also makes the event reflect that session’s geographic/device data |
engagement_time_msec |
Recommended alongside session_id (e.g. for Realtime). Google’s sample includes it; practitioners report it is not strictly required — treat as recommended, not confirmed-required |
transaction_id |
Deduplication key for purchase (see rules below) |
value |
Conversion revenue |
currency |
ISO currency code |
items |
Ecommerce items array |
Sending a new session_id value creates a new session without a
session_start event — so an absent or mismatched session_id spawns an
unintended new session instead of joining the original one.
Identity stitching — capture at the online touchpoint
Persist both signals with the order at the moment of the web interaction, then send them back verbatim on the offline event.
| Signal | Capture from | BigQuery field |
|---|---|---|
client_id |
The _ga cookie (middle two numbers only) or the gtag get() API |
user_pseudo_id |
session_id |
The _ga_<MEASUREMENT_ID> cookie or gtag |
ga_session_id |
The _ga cookie is formatted GA1.1.<clientId1>.<clientId2>; the actual
client_id is only the middle two dot-separated numbers, not the leading
GA1.1 or trailing portion. Note: the session_id to ga_session_id
1:1 mapping, and the need to send session_id (not ga_session_id), is
documented by practitioners (Simo Ahava) rather than spelled out on the
canonical Measurement Protocol reference page.
Timestamp resolution
Priority order MP uses to resolve the event time:
| Priority | Source |
|---|---|
| 1 | Event-level timestamp_micros |
| 2 | Request-level timestamp_micros |
| 3 | Time the Measurement Protocol receives the request |
| Window | Value | Meaning |
|---|---|---|
| Backdating | 72 hours | Events and user properties can be backdated up to 72 hours |
| Join receipt | 48 hours | MP events meant to join/process with Firebase SDK or gtag.js events should be received within 48 hours of the original client-side event timestamp |
validation_behavior and out-of-window timestamps
| Value | Validation | Timestamp older than 72h |
|---|---|---|
RELAXED (default) |
Only rejects malformed requests | Accepts the event but overrides its timestamp to 72h ago |
ENFORCE_RECOMMENDATIONS |
Stricter validation on event parameters and timestamps | Rejects the event |
Confirm the exact per-mode behavior for out-of-window timestamps against the current documentation before relying on it in code.
Consent object
| Key | Values | Meaning |
|---|---|---|
ad_user_data |
GRANTED / DENIED |
Consent to send user data from the request’s events/user properties to Google for advertising |
ad_personalization |
GRANTED / DENIED |
Consent for personalized advertising |
If consent is omitted, GA uses the consent settings from the client’s
corresponding online interactions. How that inheritance resolves when there is
no matching prior online interaction for a purely offline client_id is not
detailed in the docs.
transaction_id deduplication rules
| Rule | Detail |
|---|---|
| Mechanism | GA deduplicates purchase events that share the same transaction_id |
| Uniqueness | Must be unique per order; must not be reused across different users |
| Stream scope | Works only for data collected through web streams, not app streams |
| Empty string | Never send transaction_id="" — GA deduplicates ALL empty-string purchases together, drastically undercounting |
The docs do not publish an explicit deduplication time window (how far apart two identical IDs can arrive and still dedupe).
Limits
| Limit | Value |
|---|---|
| Events per request | 25 |
| Parameters per event | 25 |
| User properties per request | 25 |
| Event / parameter name length | 40 characters max |
| Parameter value length | 100 characters max (500 on GA 360) |
Validation server response
POST the identical payload to /debug/mp/collect. The response carries a
validationMessages array; each item has fieldPath, description, and
validationCode.
validationCode |
|
|---|---|
VALUE_INVALID |
VALUE_REQUIRED |
NAME_INVALID |
NAME_RESERVED |
VALUE_OUT_OF_BOUNDS |
EXCEEDED_MAX_ENTITIES |
NAME_DUPLICATED |
The validation server does NOT validate api_secret or measurement_id — a
request can pass validation yet still silently drop in production if those
credentials are wrong.
Gotchas
- Offline conversion lands as a new
(direct)session, not the original. Cause: the request omittedclient_id/session_idor sent asession_idthat matches no existing session, and any newsession_idcreates a fresh session. Fix: capture the exactclient_idandsession_idat the online touchpoint, persist them with the order, and send both back verbatim. - Purchase appears nowhere despite HTTP 2xx or a passing
/debugcheck. Cause:/mp/collectreturns 2xx regardless of a badapi_secretormeasurement_id, and the debug server does not check those — or thetimestamp_microsis beyond 72h. Fix: triple-check both credentials against the data stream, keep the timestamp inside 72h, and confirm microseconds. - Revenue is double-counted between the browser and the server send.
Cause: GA only collapses purchases sharing an identical
transaction_id; different, missing, or empty IDs are both counted. Fix: use the same real order ID on both sends and never sendtransaction_id="". client_idis rejected or fails to match. Cause: the raw_gacookie value (GA1.1.<clientId1>.<clientId2>) was used instead of the middle two numbers. Fix: strip theGA1.1.prefix to<clientId1>.<clientId2>, or prefer the gtagget()API to avoid fragile cookie parsing.- Older events silently land with a recent timestamp or drop entirely.
Cause:
timestamp_microsbeyond 72h —RELAXEDclamps to ~72h ago,ENFORCE_RECOMMENDATIONSrejects. Fix: send within 72h (ideally received within 48h); do not rely on MP for long-delayed backfills. - Ad exports or audiences omit MP conversions, or consent looks wrong.
Cause:
consentwas omitted orDENIED; when omitted, GA inherits the client’s online consent state, which may not match the offline event. Fix: set request-levelconsentexplicitly to reflect the actual consent.
Quick recipes
// At the online touchpoint (checkout / lead form), capture the two identifiers// GA4 needs to stitch a later offline conversion back to THIS session.gtag('get', 'G-XXXXXXX', 'client_id', (clientId) => { // session_id is session-scoped state in the _ga_<MEASUREMENT_ID> cookie const sessionId = readSessionId('_ga_XXXXXXX'); persistWithOrder({ client_id: clientId, session_id: sessionId });});// _ga is formatted "GA1.1.<clientId1>.<clientId2>".// client_id is ONLY the middle two dot-separated numbers.function clientIdFromGa(gaCookie) { // "GA1.1.123456789.1721470000" -> "123456789.1721470000" return gaCookie.split('.').slice(2).join('.');}curl -s "https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "client_id": "123456789.1721470000", "timestamp_micros": 1721480000000000, "consent": { "ad_user_data": "GRANTED", "ad_personalization": "GRANTED" }, "events": [{ "name": "purchase", "params": { "session_id": "1721470000", "engagement_time_msec": 1, "transaction_id": "ORDER-10492", "currency": "USD", "value": 129.00, "items": [{ "item_id": "SKU_88", "item_name": "Annual plan", "price": 129.00, "quantity": 1 }] } }] }'// Unix MICROSECONDS, not milliseconds. Stay inside the 72h backdate window.const conversionMs = orderCompletedAt.getTime(); // epoch millisecondsconst timestamp_micros = conversionMs * 1000; // -> microsecondsconst withinWindow = Date.now() - conversionMs < 72 * 3600 * 1000;curl -s "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXX&api_secret=API_SECRET" \ -H "Content-Type: application/json" -d @payload.json# pass = {"validationMessages":[]}# NOTE: this endpoint does NOT check api_secret or measurement_id.Related
Sources
Official pages these values were read from:
- Measurement Protocol overview (read 2026-07-20)
- Send Measurement Protocol events (web/gtag) (read 2026-07-20)
- Send Measurement Protocol events (read 2026-07-20)
- Measurement Protocol reference (web/gtag) (read 2026-07-20)
- Measurement Protocol reference (read 2026-07-20)
- Validate Measurement Protocol events (read 2026-07-20)
- Minimize duplicate key events with transaction IDs (read 2026-07-20)
Community references for the _ga cookie format and the session_id join,
which the canonical MP pages do not spell out:
- Session attribution with GA4 Measurement Protocol — Simo Ahava (read 2026-07-20)
- Understanding GA4 cookies — the
_gacookie (Optimize Smart) (read 2026-07-20)
Changelog
- — Initial version, verified against official documentation.