Offline conversions, refunds and server-side events need it, and its limitations surprise people.
The Measurement Protocol (MP) is an HTTP endpoint that accepts GA4 events from anywhere: your backend, a payment webhook, a CRM, a kiosk. You POST JSON to https://www.google-analytics.com/mp/collect with two query parameters: the stream's measurement_id and an api_secret created under Admin > Data streams > stream > Measurement Protocol API secrets.
The body contains a client_id (the _ga cookie value, obtainable in the browser with gtag('get', 'G-...', 'client_id', cb)) or a user_id, optional user_properties, consent, a timestamp_micros (up to 72 hours in the past), and an events array of up to 25 events, each with name and params. For the events to attach to the user's live session include session_id and engagement_time_msec in params; otherwise they count toward the user but not toward sessions or engagement.
What MP is for: completing a journey the browser cannot see, such as a purchase confirmed by a payment provider, a refund, a lead marked qualified in the CRM, or an app event from a server. Google's intended use is augmentation; it is not designed as a full server-side replacement for the tag.
What it cannot do: MP events carry no automatic parameters, so page_location, device, geo (derived from the request IP, which will be your server's), referrer and campaign are absent unless you set what you can. They are not shown in Realtime's user-source cards, do not create first_visit, and are ignored if the event name or parameters break the rules. Attribution of an MP purchase works only if the client_id matches a browser that already had sessions.
Validate with the debug endpoint /debug/mp/collect, which returns validation messages instead of silently accepting the payload like the real endpoint does. Keep the api_secret on the server; anyone holding it can inject events into your property.
Example
// Node.js: confirm a purchase from a payment webhook (api_secret stays on the server)
const MEASUREMENT_ID = 'G-ABC123XYZ';
const API_SECRET = process.env.GA_API_SECRET;
async function sendPurchase(order) {
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
const body = {
client_id: order.gaClientId, // captured in the browser with gtag('get', ...)
user_id: order.userId,
timestamp_micros: Date.now() * 1000,
events: [{
name: 'purchase',
params: {
session_id: order.gaSessionId, // ties the event to the live session
engagement_time_msec: 100,
transaction_id: order.id,
value: order.total, currency: 'EUR',
items: order.items.map(i => ({ item_id: i.sku, item_name: i.name, price: i.price, quantity: i.qty }))
}
}]
};
// swap /mp/collect for /debug/mp/collect while developing to get validation messages
const res = await fetch(url, { method: 'POST', body: JSON.stringify(body) });
console.log(res.status); // 204 on the real endpoint, even for bad payloads
}Remember
- POST JSON to /mp/collect with measurement_id and api_secret; up to 25 events per request
- Use the browser's client_id plus session_id to attach to the live session
- Augmentation only: no automatic parameters, geo or referrer; validate on /debug/mp/collect