Blog · GA4 MP
GA4 Measurement Protocol is microseconds. Pasting it into Meta CAPI is 1970.
google-analytics.com/mp/collect wants timestamp_micros, 16-ish digits, 72-hour backdate. Meta wants event_time seconds. LinkedIn wants milliseconds. OpenAI wants timestamp_ms. A shared Date.now() helper cannot feed all four.
Date.now() is milliseconds, thirteen digits. That is already the OpenAI unit. It is already close to LinkedIn's conversionHappenedAt. It is a thousand times too large for Meta event_time. It is a thousand times too small for GA4 timestamp_micros. Send that thirteen-digit value as microseconds and GA4 records an event a few weeks after 1 January 1970. Send a ten-digit Meta helper as timestamp_micros and you are even closer to epoch. The 1970 screenshot is a unit bug, not a timezone bug.
This is not Google Ads conversions. Enhanced conversions is a different product on a different host with hashed email on the Google tag. Measurement Protocol is how you POST events into a GA4 property. Mixing the two in one worker is how a purchase lands in Analytics and never trains bidding, or the reverse.
The clock GA4 actually reads
The Measurement Protocol reference names timestamp_micros as an optional number: a Unix timestamp, microseconds, not milliseconds. Set it only to record events that happened in the past. Events can be backdated up to 72 hours. The same key can appear on the request, on an event, or on a user property. The protocol uses the first timestamp it finds: event or user-property timestamp_micros, then the request timestamp_micros, then the time the request is received.
If timestamp_micros is older than 72 hours and validation_behavior is omitted or RELAXED, GA4 accepts the event and rewrites the timestamp to 72 hours ago. If validation_behavior is ENFORCE_RECOMMENDATIONS, it rejects that event. Advertising joins with gtag or Firebase events want the Measurement Protocol event within 48 hours of the original client-side timestamp or attribution may not process as expected. Those are two different windows. Do not collapse them.
The collect POST returns 2xx if the HTTP request is received. It does not return an error code if the payload is malformed or if Analytics does not process the data. RELAXED is the default: malformed requests fail, invalid field names and wrong types may still be accepted, over-limit parameters are ignored. ENFORCE_RECOMMENDATIONS is for validation. Production traffic should omit validation_behavior unless you have a reason to prefer rejects over collection.
The rest of the envelope is not CAPI either
POST https://www.google-analytics.com/mp/collect, or region1.google-analytics.com/mp/collect for EU collection. Query params: api_secret, plus measurement_id for a web stream or firebase_app_id for an app stream. The JSON body needs client_id on web or app_instance_id for Firebase. events[] is required, up to 25 events, each with name. Body under 130kB. session_id and engagement_time_msec are how Realtime and engagement reports stay honest.
Google's own overview says the protocol is meant to augment gtag, Tag Manager, or Firebase, not replace them. Full server-to-server is possible and only partial reporting may be available. Remarketing, advertising identifiers, and privacy settings join on client_id or app_instance_id. Reserved event names such as session_start and first_visit cannot be sent this way. screen_view, ad_impression, and in_app_purchase are app-stream-only; web uses page_view and purchase.
Debug is a different path: /debug/mp/collect. Events sent there do not appear in reports. The debug host does not even validate api_secret or firebase_app_id. A green debug response with the wrong secret is still a production miss. Use Event Builder or ENFORCE_RECOMMENDATIONS while you are wiring the clock. Then POST to /mp/collect without the debug prefix.
Four vendors, four clocks
Meta Conversions API event_time is a Unix timestamp in seconds, GMT, required. Search-indexed Meta docs describe a seven-day lookback: if any event_time in the batch is older than seven days, the entire request errors and no events process. Math.floor(Date.now() / 1000) is the helper. A 13-digit millisecond value is tens of thousands of years in the future. A 16-digit microsecond value is worse.
LinkedIn Conversions API conversionHappenedAt is epoch milliseconds. The schema says so, with example 1590739275000. If your source records seconds, insert 000 at the end. The timestamp must be within the past 90 days. INVALID_CONVERSION_TIME_FIELD_VALUE is the production error when it is not. Copying Meta event_time without multiplying by 1000 is how LinkedIn sees a time near 1970 or outside the window, depending on how far you truncated.
OpenAI Ads Conversions API timestamp_ms is an integer Unix time in milliseconds, required, within the last seven days and no more than ten minutes in the future. Date.now() is already that unit. Dividing by 1000 for Meta and reusing the result as timestamp_ms makes the event too old or invalid. OpenAI's field is not timestamp_micros and it is not event_time.
A function named now() that always returns Date.now() can feed OpenAI. It can feed LinkedIn if you keep milliseconds. It cannot feed Meta without dividing by 1000. It cannot feed GA4 without multiplying by 1000. The conversion belongs at the vendor edge, in four differently named fields, not in a shared JSON key called timestamp.
The helper that lands in 1970
Date.now() written to timestamp_micros. Thirteen digits are milliseconds. GA4 asked for microseconds.
{
"client_id": "1234567890.1234567890",
"timestamp_micros": 1743422400000,
"events": [{
"name": "purchase",
"params": {
"currency": "USD",
"value": 42.00,
"transaction_id": "order_8891"
}
}]
}
1743422400000 microseconds after epoch is still January 1970. Analytics may 2xx. Realtime may show nothing useful. The same integer pasted into Meta event_time is a far-future second and falls outside the seven-day window. The same integer is a plausible LinkedIn conversionHappenedAt and a plausible OpenAI timestamp_ms. One helper, four interpretations, one of them is 1970, one of them is fifty thousand years out.
Multiply Date.now() by 1000 for timestamp_micros when you are backdating a GA4 event. Divide Date.now() by 1000 and floor it for Meta event_time. Pass Date.now() as OpenAI timestamp_ms. Pass milliseconds, or seconds with 000 appended, as LinkedIn conversionHappenedAt. Persist the UTC instant once. Convert at emit.
Names that do not translate
- GA4 timestamp_micros is microseconds, optional, 72-hour backdate.
- Meta event_time is seconds, required, about a 7-day window.
- LinkedIn conversionHappenedAt is milliseconds, past 90 days. Append 000 if you only have seconds.
- OpenAI timestamp_ms is milliseconds, last 7 days, not more than 10 minutes in the future.
- Date.now() is milliseconds. It is not a universal event_time.
- mp/collect 2xx is not validation. Use /debug/mp/collect.
- Measurement Protocol is not Google Ads enhanced conversions.
Joins still need the client id
Web Measurement Protocol without a client_id that matches the Google tag does not join the user. App protocol without app_instance_id from the Firebase SDK is a different miss. session_id should match the SDK session if you want the event in the same session. engagement_time_msec is milliseconds of engagement since the previous event, not another name for timestamp_micros.
Google will derive geo and device from recent tagged hits on that client_id unless you send user_location or device. ip_override is an alternative to user_location, and user_location wins if both are present. None of that repairs a 13-digit timestamp_micros. The clock is still 1970.
Consent on Measurement Protocol is ad_user_data and ad_personalization, GRANTED or DENIED. That is Consent Mode language on a server POST. It is still not a Google Ads click conversion. If the business question is Google Ads ROAS, you want a conversion action and, where you use it, enhanced conversions. mp/collect will not upload that.
Optional. A Unix timestamp, microseconds, not milliseconds. Represents the time of the event. Should be set only to record events that happened in the past. Can be overridden by user_property or event timestamps. Events can be backdated up to 72 hours.
GA4 Measurement Protocol reference, timestamp_micros
What to do
Keep one UTC instant internally. At the GA4 edge, write timestamp_micros as that instant in microseconds, only for past events inside 72 hours. At the Meta edge, write event_time in seconds. At LinkedIn, write conversionHappenedAt in milliseconds. At OpenAI, write timestamp_ms in milliseconds. Do not share a Date.now() export named event_time.
POST mp/collect with api_secret and the stream id that matches the property. Include client_id or app_instance_id. Validate on /debug/mp/collect, then strip debug. Do not send Measurement Protocol JSON to graph.facebook.com, api.linkedin.com, or bzr.openai.com.
The GA4 pack is vendor/google-analytics. Enhanced conversions is a different docs page and a different Google product. Pixellint is not affiliated with Google, Meta, LinkedIn, or OpenAI. Passing a linter means the artifact matches the published envelope. It does not mean the property attributed a session, and it does not detect fraud.
Checklist
- POST https://www.google-analytics.com/mp/collect (or region1) with api_secret and measurement_id or firebase_app_id.
- Send timestamp_micros as microseconds, about 16 digits, only to backdate within 72 hours.
- Do not reuse Math.floor(Date.now() / 1000) as timestamp_micros.
- Do not paste timestamp_micros into Meta event_time, LinkedIn conversionHappenedAt, or OpenAI timestamp_ms.
- Validate on /debug/mp/collect. Production /mp/collect 2xx is not that check.
- If you need Google Ads conversions, use that product. Measurement Protocol is GA4.
Sources
Contract pages
The dated argument is above. These pages are the field lists.