When Your Checkout Isn't Yours: Server-Side Tracking That Survives It

How a live-events business recovered purchase visibility across a third-party checkout — Stape-hosted server-side GTM, Enhanced Conversions, and a Stripe→GA4 fallback route that logs before it sends.

A live-events business was selling tickets through a third-party registration platform, with payments landing in Stripe. Marketing ran on Google Ads. The problem: the checkout pages weren't theirs to instrument. Browser tags missed a meaningful share of purchases — ad blockers, Safari's tracking prevention, and page templates they couldn't edit — so Google Ads was optimizing bids against incomplete conversion data, and nobody could say with confidence how many tickets any campaign had actually sold.

This is a common shape of problem: the money flows through systems you don't control, and every "just add the pixel" answer assumes a checkout you can edit. Here's how we built around it, what broke along the way, and the one pattern I now consider non-negotiable.

The architecture: move the tracking off the browser

The core move is server-side tagging. Instead of the visitor's browser talking directly to Google's servers (where it gets blocked), the browser talks to a tagging server on the business's own subdomain, and that server talks to Google, GA4, and the CRM from the back end.

We deployed a Stape-hosted server-side GTM (sGTM) container. Stape runs the infrastructure; the container lives on a first-party subdomain of the client's own domain. Three Stape features earned their keep:

Enhanced Conversions without touching the checkout

Google Ads Enhanced Conversions match conversions back to ad clicks using hashed first-party data — the buyer's email and phone. In a server-side setup, the browser never sends that data to Google directly. The web GTM container reads the buyer's email and phone from the dataLayer, packages them into the GA4 event's user_data object, and ships the event to the sGTM server. The Google Ads conversion tag on the server reads that object, and Google hashes the values server-side before matching. You never hash anything yourself, and the data never rides in a third-party browser request.

Two small details in the web-container variable mattered more than the diagrams suggest: if the email is missing, send nothing at all — a phone-only record is useless for matching and drags the match rate down. And if the phone is an empty string, drop the key entirely rather than sending "", which can fail validation downstream. Ten lines of custom JavaScript, but they're the difference between a match rate you brag about and one you debug.

Where the reference architecture failed

The plan called for Stripe's purchase webhooks to post to the sGTM container's measurement-protocol endpoint, which would forward them to GA4. On paper this is the textbook route. In practice, the endpoint returned HTTP 400 on every server-to-server call — through every permutation of client priority, payload shape, and configuration we tried. The browser-originated events flowed fine; the server-originated ones never did. Our best diagnosis pointed at the hosting layer's request routing, which we didn't control.

We timeboxed the debugging, then rerouted: Stripe webhooks now post to a Google Apps Script web app, which sends purchase events straight to GA4's Measurement Protocol endpoint — no sGTM in that path at all. The sGTM container kept the jobs it was demonstrably good at: browser-side events, Enhanced Conversions, and upserting buyers into the CRM.

One quirk worth knowing: GA4's Measurement Protocol returns 204 No Content on success, not 200. If your health check tests for exactly 200, your working integration will report as broken.

function sendPurchaseToGA4(tx) {
  var url = 'https://www.google-analytics.com/mp/collect'
          + '?measurement_id=' + MEASUREMENT_ID
          + '&api_secret=' + API_SECRET;   // registered in GA4 Admin → Data Streams

  auditSheet.appendRow([new Date(), tx.id, tx.value, 'attempting']);  // log BEFORE send

  var res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify({
      client_id: tx.clientId,
      events: [{ name: 'purchase', params: {
        transaction_id: tx.id, value: tx.value, currency: 'USD'
      }}]
    }),
    muteHttpExceptions: true
  });

  // GA4 MP returns 204 on success — not 200
  auditSheet.appendRow([new Date(), tx.id, tx.value, 'HTTP ' + res.getResponseCode()]);
}

The non-negotiable: log before you send

Notice the audit sheet write before the network call. Every purchase event is appended to a spreadsheet first, then sent, then the response code is logged next to it. If GA4 ever stops receiving, the sheet shows exactly which transactions went missing and what the API said at the time. Analytics pipelines fail silently by default — a dropped purchase event doesn't throw an error anyone sees; it just quietly corrupts every report and every bidding decision downstream. The audit sheet turns "our numbers look low, I think?" into a row-by-row diff you can act on.

Costs and failure modes

Running costs are modest: Stape's hosted sGTM tier runs roughly $20–100/month depending on request volume, Apps Script is free at this scale, and GA4's Measurement Protocol costs nothing. The real cost is engineering time, and most of ours went to the discovery that the reference architecture's server-to-server path didn't work on this stack.

Failure modes to design for, in order of likelihood: an API secret that quietly expires or gets deleted in GA4 admin (verify it first when you see 400s); duplicate events when a webhook retries and your endpoint isn't idempotent (key on the transaction ID); and the checkout platform changing its page structure and silently breaking whatever dataLayer values you depend on. Each of these is survivable if — and only if — something is logging what was sent and what came back.

The lesson that generalizes: test the actual request path, not the diagram. Reference architectures describe what should work; production tells you what does. Keep a fallback route you fully control, and make every event leave a paper trail before it leaves the building.

Have a workflow like this eating your team's hours?

Iluxxian builds AI agents and automations with data-engineering discipline. Fixed scope, documented handoff.

Start a build
Get Field Notes in your inbox — one email a week, no hype.

Double opt-in — confirm via email. Unsubscribe anytime.