pixellint

CAPI · engineers · PMs

sGTM is a proxy. CAPI is your job.

sGTM still starts with a browser request to your collector host, then your container fans out. Direct CAPI starts with an order webhook. Pick based on whether you already trust GTM as the control plane.

sGTM is still a browser hop

The page hits a first-party collector (often a subdomain you CNAME). The server-side container maps that hit to GA4, Meta, ads pixels, whatever you wired. Marketers keep living in GTM. You still have to map event names, hashing, event_id, and clocks. A misconfigured GA4 client will swallow clicks and never call Meta.

Ad blockers that list the collector host, or that block the client gtm.js load, still punch holes. First-party collection helps; it is not invisibility. Pixellint's google-tag-manager pack is the loader contract (id on googletagmanager.com), not a validator for your sGTM Meta tag's JSON.

Direct CAPI is a backend job

The source of truth is the shop backend, the warehouse, or a queue on the payment webhook. You write a small client per vendor. You do not inherit GTM preview, and you do not inherit GTM foot-guns. You also do not inherit a marketer-friendly UI for adding a new event.

Direct CAPI still has to send event_time in seconds for Meta, ISO 8601 for TikTok, hashed em not hashed IP, event_source_url for website, value and currency on Purchase. The transport changed. The contract did not.

Hybrid is the common production shape

Browser tags into sGTM for PageView and view-content. Backend CAPI for Purchase. Dedup still needs a shared event_id. Two teams owning two pipes without that id is how ROAS doubles overnight.

Decide which pipe is allowed to send Purchase. If both send it, they must share the key. If only the backend sends Purchase, do not also fire a Purchase tag in sGTM 'for coverage'. Coverage without dedup is double-counting.

GA4 MP through sGTM is still MP

Measurement Protocol still wants api_secret, exactly one of measurement_id or firebase_app_id, and timestamp_micros at 16 digits when you set it. vendor.google-analytics.stream.identifier_missing and .identifier_ambiguous are the stream identity bugs. HTTP 204 from /mp/collect is not a schema check.

session_id and engagement_time_msec still matter for sessions on GA4. A server event with no session join looks like a user who appeared, purchased, and vanished. That is a reporting problem, not a CAPI problem, and it shows up when sGTM forwards ecommerce without the session params the web tag would have sent.

CNAME is not the architecture

Pointing collect.example.com at a tagging server is a DNS choice. It does not pick sGTM versus a custom proxy versus a warehouse emitter. Cloaking a third-party pixel host to evade blockers has a different failure mode (and a different article). Server-side tagging that you own is a first-party collector you configured, not a disguise.

If the only reason you bought sGTM was to hide facebook.com/tr, you still need CAPI for the users who never loaded the container, and you still need event_id if both run.

Two collectors, one Purchase

sGTM can forward the browser event. The shop can POST CAPI. Same event_id or you count twice.

// Browser -> sGTM (PageView, ViewContent). Purchase is NOT in this container.

// Shop backend -> Meta CAPI on payment capture
POST https://graph.facebook.com/v21.0/{pixel-id}/events?access_token=...
{
  "data": [{
    "event_name": "Purchase",
    "event_id": "order-1842",
    "event_time": 1770000000,
    "action_source": "website",
    "event_source_url": "https://shop.example/thanks?order=1842",
    "user_data": { "em": ["..."], "fbp": "fb.1.1770000000.1", "client_ip_address": "203.0.113.10", "client_user_agent": "Mozilla/5.0" },
    "custom_data": { "value": 84.50, "currency": "USD" }
  }]
}

// If sGTM also sends Purchase, it must reuse event_id order-1842, not mint another.

Check the artifact

Paste the pixel URL or JSON body into the playground. Same engine as pixellint validate. Nothing leaves the tab.