Case Study — Shopify → Pinterest
Ad blockers, Safari's ITP, and iOS quietly delete a chunk of every store's purchase signal before it reaches the ad platform. I built the server-side relay that reports it directly instead — hashed, deduplicated, and verified against a real order, not a demo.
7-day spike · verified end-to-endMost Shopify stores report a purchase to their ad platform with a single browser pixel firing after checkout. That pixel is exactly what ad blockers, tracking prevention, and iOS are built to stop — so the platform underreports conversions, optimizes against incomplete data, and the store never finds out.
A Conversions API (CAPI) fixes this by having the store's own server tell the ad platform "this person converted" — a channel no browser extension can touch. Meta, Pinterest, TikTok, and Snap all expose one, and they're structurally identical: a pixel and a server, a shared event id, hashed identity, a dedup window. I learned the mechanics on Meta's docs — the best-written of the four — and built the working pipe against Pinterest.
A web pixel extension runs inside Shopify's locked-down storefront
sandbox, catches the checkout_completed event, and hands the
raw fields to my app's server. The server — not the browser — stamps the
shopper's real IP and user-agent, hashes any PII, and relays a Pinterest
Conversions API event keyed to the order id.
The web pixel subscribes to checkout_completed — the most
reliable purchase signal Shopify exposes, since it only fires after the
order is confirmed server-side — and pulls out just the fields the CAPI
payload needs.
// extensions/capi-web-pixel/src/index.ts
analytics.subscribe("checkout_completed", (event) => {
const extracted = {
order_id: checkout.order?.id ?? checkout.token, // dedup key
email: checkout.email, // hashed on the server, never here
total: checkout.totalPrice?.amount,
currency: checkout.currencyCode,
lineItems: checkout.lineItems?.map(item => ({...})),
};
fetch(settings.collectEndpointUrl, {
method: "POST",
body: JSON.stringify(extracted),
});
});
Pinterest needs at least one identity signal per event — a hashed email, or the client IP and user-agent. The pixel sandbox has no access to the real client IP, so the server reads it off the request headers instead, checking the common proxy/CDN variants in priority order.
// app/routes/api.collect.tsx
const clientIpAddress =
request.headers.get("cf-connecting-ip") ??
request.headers.get("true-client-ip") ??
request.headers.get("x-real-ip") ??
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
const clientUserAgent = request.headers.get("user-agent");
Email and phone are normalized (trim, lowercase) and SHA-256 hashed in a platform-agnostic module — written once against Meta's spec, reused verbatim for Pinterest, since both require the same normalize-then-hash rule.
// app/lib/capi/hash.ts
export function normalizeAndHash(raw?: string | null): string | undefined {
const normalized = raw?.trim().toLowerCase();
return normalized ? sha256(normalized) : undefined;
}
A 4xx from Pinterest (bad payload, missing identity) won't succeed on a second try, so it returns immediately. A network error or 5xx gets exponential backoff. The function never throws — a failed forward gets logged, not crashed on.
// app/lib/capi/pinterestClient.ts if (response.ok || response.status < 500) { return { ok: response.ok, status: response.status, result }; } // 5xx and network errors: exponential backoff, then give up cleanly await delay(retryDelayMs * 2 ** attempt);
marketingAllowed signal and skips the forward entirely on an explicit opt-out, rather than collecting and filtering downstream.6722974548010 — with Pinterest returning num_events_processed: 1, no forward error.