CLIENT IMPLEMENTATION HANDOFF · 14 OCTOBER 2026
Finish OpenAI conversion tracking
Shopify default cookie banner + Cloudflare Worker
Install in order • Test consent and matching • Enable live recording last
Read first: status and launch condition
We have updated the Shopify custom purchase pixel. Its purchase switch is currently false. The client owns the remaining Cloudflare and theme work. Follow these steps in order; do not create another pixel, Worker or webhook.
| Item | Existing value |
|---|---|
| Pixel ID | BgCs8neHjGo12KJT4wL814 |
| Worker | openai-shopify-conversions |
| Webhook URL | https://openai-shopify-conversions.sewheartfelt.co.uk/webhooks/orders |
| Shopify webhook | Order creation, JSON, API version 2026-10 |
Required before live activation: complete the consent tests in Step 5. If an order placed after rejecting or withdrawing cookies still shows granted, keep live CAPI disabled and tell us.
This measures orders created, which may include unpaid orders. It has not been tested against this store. Never put API keys or the Shopify signing secret in theme or custom pixel code.
1. Back up and pause server processing
- Open Cloudflare → Workers & Pages → openai-shopify-conversions → Settings → Variables and secrets.
- Edit
MEASUREMENT_ENABLEDto the Text valuefalse. Save and deploy. - Click Edit code. Copy the current Worker code into a private backup file.
- In Shopify → Online Store → Themes, use the live theme’s three-dot menu → Duplicate. Name the backup with today’s date.
During the pause, signed order webhooks receive 503 and Shopify may retry. Keep this installation window short. Validation is for controlled tests, not days of normal customer traffic.
2. Replace the Worker code
- Cloudflare → Workers & Pages → openai-shopify-conversions → Edit code.
- Open the existing main source file, usually
worker.jsorindex.js. - Select all its contents. Paste the complete block below. Do not add HTML script tags.
- Click Deploy.
- Open the Worker base URL. Expect
Shopify conversion Worker is running.
The code below supersedes the original guide. It adds hashed customer IDs, outcome logs, and test-webhook protection. The consent check is mandatory; the old REQUIRE_ORDER_CONSENT toggle cannot bypass it.
const PIXEL_ID = "BgCs8neHjGo12KJT4wL814";
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (request.method === "GET" && path === "/")
return new Response("Shopify conversion Worker is running.");
if (path !== "/webhooks/orders") return new Response("Not found", {status:404});
if (request.method !== "POST") return new Response("POST required", {status:405});
for (const name of ["OPENAI_CONVERSIONS_API_KEY", "SHOPIFY_WEBHOOK_SECRET", "SHOPIFY_STORE_DOMAIN", "STORE_URL"]) {
if (!env[name]) return new Response("Missing configuration: " + name, {status:503});
}
try {
const body = await request.arrayBuffer();
if (!await verify(body, request.headers.get("X-Shopify-Hmac-Sha256"), env.SHOPIFY_WEBHOOK_SECRET))
return new Response("Invalid signature", {status:401});
const shop = request.headers.get("X-Shopify-Shop-Domain");
if (shop !== env.SHOPIFY_STORE_DOMAIN) return new Response("Unexpected store", {status:403});
if (request.headers.get("X-Shopify-Topic") !== "orders/create")
return new Response("Expected orders/create", {status:400});
if (env.MEASUREMENT_ENABLED !== "true")
return outcome("measurement_disabled", 503);
const validating = env.OPENAI_VALIDATE_ONLY !== "false";
if (request.headers.get("X-Shopify-Test") === "true" && !validating)
return outcome("shopify_test_skipped");
const order = JSON.parse(new TextDecoder().decode(body));
if (order.source_name !== "web" || order.cancelled_at) return outcome("non_web_or_cancelled_order_skipped");
const time = Date.parse(order.created_at);
if (!numericId(order.id) || !Number.isFinite(time)) return new Response("Invalid order", {status:400});
// Required-consent mode: developer must populate this attribute from actual consent.
const attrs = Object.fromEntries((order.note_attributes || []).map(a => [a.name, a.value]));
if (attrs.openai_measurement_consent !== "granted")
return outcome("consent_missing_or_denied");
const event = {
id: "shopify_order_" + order.id,
type: "order_created", timestamp_ms: time,
action_source: "web",
source_url: new URL("/checkout", env.STORE_URL).href,
data: {type:"contents"}
};
if (typeof attrs.oppref === "string" && attrs.oppref.trim()) event.oppref = attrs.oppref;
const currency = String(order.currency || "").toUpperCase();
const total = Number(order.total_price);
if (/^[A-Z]{3}$/.test(currency) && Number.isFinite(total)) {
const digits = new Intl.NumberFormat("en", {style:"currency", currency}).resolvedOptions().maximumFractionDigits;
const amount = Math.round(total * 10 ** digits);
if (Number.isSafeInteger(amount)) Object.assign(event.data, {amount, currency});
}
const user = {};
const email = order.email || order.contact_email;
if (env.SEND_HASHED_EMAIL === "true" && typeof email === "string" && email.trim())
user.emails_sha256 = [await sha256(email.trim().toLowerCase())];
const customerId = numericId(order.customer?.id);
if (env.SEND_HASHED_EXTERNAL_ID === "true" && customerId)
user.external_ids_sha256 = [await sha256("shopify_customer_" + customerId)];
if (typeof attrs.openai_obref === "string" && attrs.openai_obref.trim())
user.obref = attrs.openai_obref;
if (Object.keys(user).length) event.user = user;
const upstream = await fetch("https://bzr.openai.com/v1/events?pid=" + encodeURIComponent(PIXEL_ID), {
method:"POST",
headers:{Authorization:"Bearer " + env.OPENAI_CONVERSIONS_API_KEY, "Content-Type":"application/json"},
body:JSON.stringify({validate_only:validating, integration_source:"shopify_cloudflare_worker", events:[event]}),
signal:AbortSignal.timeout(3000)
});
const diagnostic = {
upstream_status: upstream.status, validating,
has_email: Boolean(user.emails_sha256),
has_external_id: Boolean(user.external_ids_sha256),
has_obref: Boolean(user.obref), has_oppref: Boolean(event.oppref)
};
// Do not log response bodies, orders, emails, keys or click identifiers.
if (!upstream.ok) return outcome("openai_rejected", 502, diagnostic);
return outcome(validating ? "validated_not_recorded" : "submitted", 200, diagnostic);
} catch {
return outcome("processing_failed", 503);
}
}
};
async function verify(body, signature, secret) {
if (!signature) return false;
try {
const bytes = Uint8Array.from(atob(signature), c => c.charCodeAt(0));
if (bytes.length !== 32) return false;
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {name:"HMAC", hash:"SHA-256"}, false, ["verify"]);
return await crypto.subtle.verify("HMAC", key, bytes, body);
} catch { return false; }
}
function outcome(outcome, status = 200, details = {}) {
console.log("OpenAI conversion", {outcome, ...details});
return new Response(outcome, {status});
}
function numericId(value) {
if (typeof value === "number" && !Number.isSafeInteger(value)) return null;
const id = String(value ?? "").split("/").pop();
return /^\d+$/.test(id) ? id : null;
}
async function sha256(value) {
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
return Array.from(new Uint8Array(bytes), b => b.toString(16).padStart(2,"0")).join("");
}
3. Set the Worker configuration
Cloudflare → Worker → Settings → Variables and secrets. Use the pencil to edit a value; use Add variable for missing settings. Values below are text strings without quotes. Save/deploy changes.
| Name | Type | Setup value |
|---|---|---|
| MEASUREMENT_ENABLED | Text | false until Step 5 |
| OPENAI_VALIDATE_ONLY | Text | true |
| SEND_HASHED_EMAIL | Text | true |
| SEND_HASHED_EXTERNAL_ID | Text | true, new setting |
| REQUIRE_ORDER_CONSENT | Text | true, retained for clarity; code always requires mapped permission |
| SHOPIFY_STORE_DOMAIN | Text | sew-heart-felt.myshopify.com |
| STORE_URL | Text | https://www.sewheartfelt.co.uk/ |
| OPENAI_CONVERSIONS_API_KEY | Secret | Keep the existing OpenAI conversion key |
| SHOPIFY_WEBHOOK_SECRET | Secret | Keep the existing Shopify webhook signing secret |
No key regeneration or new webhook is required. Existing webhook: Shopify → Settings → Notifications → Webhooks. Confirm its HTTPS URL exactly matches the URL at the top of this report.
4. Replace the existing theme base script
- Shopify → Online Store → Themes → live theme → … → Edit code.
- Open Layout → theme.liquid.
- Search within the file for
oaiqandbzrcdn.openai.com. Identify the OpenAI-only base pixel block, including its opening and closing script tags. - Replace that OpenAI-only block with the full block below, inside
<head>after Shopify’s{{ content_for_header }}. Leave that Shopify Liquid expression in place. If the old block mixes other services or is injected by an app, have the theme developer remove only the old OpenAI installation. Do not add a second initialisation. - Keep
OPENAI_THEME_MEASUREMENT_ENABLED = falseduring validation, then click Save. - Shopify → Settings → Customer privacy → Cookie banner: verify the Shopify banner is enabled for the relevant customer regions and has a working consent-preference/rejection path.
The code reads Shopify permissions; it does not grant consent or subscribe anyone to email marketing. It requires both marketing and data-sharing permission. “granted” below is an internal allowed-processing marker, not proof of a new banner click. Shopify’s allowed methods account for region and store settings.
<!-- OpenAI consent and cart bridge. Replace the existing OpenAI base script. -->
<script>
(function () {
if (window.__openaiConsentBridge) return;
window.__openaiConsentBridge = true;
const PIXEL_ID = "BgCs8neHjGo12KJT4wL814";
// Leave false until ALL launch checks in this report pass.
const OPENAI_THEME_MEASUREMENT_ENABLED = false;
if (!window.oaiq) {
const q = function () { q.q.push(arguments); };
q.q = [];
window.oaiq = q;
const script = document.createElement("script");
script.async = true;
script.src = "https://bzrcdn.openai.com/sdk/oaiq.min.js";
document.head.appendChild(script);
}
window.oaiq("consent", false);
window.oaiq("init", {pixelId: PIXEL_ID});
let chain = Promise.resolve();
let started = false;
function allowed() {
const p = window.Shopify?.customerPrivacy;
return Boolean(p && p.marketingAllowed() === true && p.saleOfDataAllowed() === true);
}
function cookie(name) {
const row = document.cookie.split(";").map(s => s.trim())
.find(s => s.startsWith(name + "="));
if (!row) return "";
try { return decodeURIComponent(row.slice(name.length + 1)); }
catch { return ""; }
}
function sync() {
// Stop browser measurement immediately on denial, before cart network work.
window.oaiq("consent", OPENAI_THEME_MEASUREMENT_ENABLED && allowed());
chain = chain.catch(() => {}).then(async () => {
const permitted = allowed();
const attrs = {
openai_measurement_consent: permitted ? "granted" : "denied",
openai_consent_updated_at: new Date().toISOString(),
oppref: "",
openai_obref: ""
};
if (permitted) {
attrs.oppref = new URL(location.href).searchParams.get("oppref") || cookie("__oppref");
attrs.openai_obref = cookie("__obref");
}
const root = window.Shopify?.routes?.root || "/";
const response = await fetch(root + "cart/update.js", {
method: "POST",
headers: {"Content-Type": "application/json"},
credentials: "same-origin",
body: JSON.stringify({attributes: attrs})
});
if (!response.ok) throw new Error("Cart consent update failed");
console.log("OpenAI cart consent", {state: attrs.openai_measurement_consent});
});
chain.catch(() => console.error("OpenAI cart consent sync failed; do not enable live CAPI."));
return chain;
}
// For theme/developer verification. Does not change the shopper's choice.
window.openaiSyncCartConsent = sync;
// Before checkout, finish saving the latest consent choice to the cart.
// Waits at most 2 seconds, so checkout is never blocked.
function beforeCheckout(go) {
Promise.race([sync(), new Promise(r => setTimeout(r, 2000))])
.catch(() => {}).then(go);
}
document.addEventListener("submit", function (e) {
const form = e.target;
const button = e.submitter;
if (!button || button.name !== "checkout" || form.dataset.openaiChecked) return;
e.preventDefault();
beforeCheckout(function () {
form.dataset.openaiChecked = "true";
const flag = document.createElement("input");
flag.type = "hidden";
flag.name = "checkout";
flag.value = button.value || "";
form.appendChild(flag);
form.submit();
});
}, true);
document.addEventListener("click", function (e) {
const link = e.target.closest && e.target.closest('a[href*="/checkout"]');
if (!link || e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey) return;
e.preventDefault();
beforeCheckout(function () { location.href = link.href; });
}, true);
function start() {
if (started) return;
started = true;
document.addEventListener("visitorConsentCollected", sync);
window.addEventListener("pageshow", sync);
window.addEventListener("focus", sync);
sync();
}
function load(attempt = 0) {
if (window.Shopify?.customerPrivacy) return start();
if (window.Shopify?.loadFeatures) {
window.Shopify.loadFeatures([{name:"consent-tracking-api",version:"0.1"}], error => {
if (error) console.error("OpenAI: Shopify privacy API unavailable.");
else start();
});
} else if (attempt < 40) {
setTimeout(() => load(attempt + 1), 250);
} else console.error("OpenAI: Shopify privacy API unavailable.");
}
load();
})();
</script>No extra developer work needed: this code also saves the shopper’s latest cookie choice to the cart just before they go to checkout, from the cart page or the cart drawer. “Buy it now” and express buttons on a product page skip the cart, so those orders arrive without a consent marker and the Worker does not send them. That under-counts slightly but never sends a conversion without consent.
5. Run controlled validation tests
First test cart mapping, then API validation. Open a fresh private browser session. Use a controlled test order approved by the store owner. Avoid enabling Shopify test payment mode on a busy live store without coordinating with the owner.
- With no banner choice, add an item to the cart. Open browser Developer Tools → Console. Run the cart check below. Expect
deniedin regions requiring consent. - Accept the relevant permissions through the banner. Run the check again. Expect
granted. It calls the sync helper and waits for the update, so this checks the bridge rather than a checkout race. - Test the store’s ordinary checkout without manually calling the helper. This checks the built-in checkout step in the theme code.
- Enable Cloudflare
MEASUREMENT_ENABLED = true, keepOPENAI_VALIDATE_ONLY = true, save/deploy. Keep both browser switches false during server validation. - Complete the accepted-consent order. Shopify → Orders → open that order → Additional details: find
openai_measurement_consent: granted. If not displayed, the developer must inspect the order webhook’s note_attributes securely. - Cloudflare → Worker → Observability → Logs. Open the corresponding POST and its application log entries. Expect
validated_not_recorded, upstream status 2xx, andhas_email: trueif email exists. Expecthas_external_id: truewhen the order has a customer ID. - In a separate private session, reject cookies and complete another controlled order. Expect denied or missing consent on the order, Worker log
consent_missing_or_denied, and no upstream OpenAI call. - Accept, then withdraw through the privacy preferences. Repeat the checkout. It must be skipped. Also test consent changes available inside checkout and each accelerated path, including Shop Pay or Buy it now if offered.
- After theme/browser measurement is enabled for a controlled live test, verify
opprefandopenai_obrefforwarding where actually available. A missing click ID on a direct visit is normal. Do not manufacture a click identifier.
await window.openaiSyncCartConsent();
const cart = await fetch(window.Shopify.routes.root + "cart.js").then(r => r.json());
console.log(cart.attributes.openai_measurement_consent);A 200 webhook status alone is insufficient: it can mean an order was skipped. Validation-mode orders are not recorded and are acknowledged to Shopify, so they will not automatically become live conversions later. Pause server processing again if controlled validation is complete but launch checks are unresolved.
Shopify’s “Send test” confirms transport/signing if it reaches a post-verification outcome. The sample often lacks the web-order source or consent attribute and may be skipped. It is not a substitute for a controlled storefront purchase.
6. Activate only after all checks pass
Before activation, confirm: accepted consent validates; rejected and withdrawn consent never sends; every checkout path is covered; theme initialisation is consent-controlled; the Worker sends available hashes; and the custom pixel has no runtime errors in Shopify’s pixel test tools.
- Cloudflare → Worker → Settings → Variables and secrets: set
OPENAI_VALIDATE_ONLY = false; keepMEASUREMENT_ENABLED = true. Save/deploy. - Shopify → Online Store → Themes → … → Edit code → theme.liquid: change only
OPENAI_THEME_MEASUREMENT_ENABLED = falsetotrue. Save. - Shopify → Settings → Customer events → existing OpenAI pixel: change only
OPENAI_PURCHASE_ENABLED = falsetotrue. Save. - Complete a new accepted-consent controlled purchase. Confirm Worker
submittedand the expected matching booleans. - Ads Manager → Conversions → SHF OpenAI Pixel → event diagnostics: inspect the new
order_createdevent. Confirm value/currency and matching data. The exact diagnostics labels may differ. - Confirm browser and server use identical pixel ID, event name and
shopify_order_<numeric order ID>. Verify one purchase after deduplication. The first received duplicate wins, so verify the browser event’s matching data as well. - Check ad attribution separately using an eligible real ad interaction. A direct test order can be received without an attributed campaign conversion.
| Final setting | Value after successful verification |
|---|---|
| MEASUREMENT_ENABLED | true |
| OPENAI_VALIDATE_ONLY | false |
| SEND_HASHED_EMAIL | true |
| SEND_HASHED_EXTERNAL_ID | true |
| REQUIRE_ORDER_CONSENT | true; mandatory in code |
| Theme OPENAI_THEME_MEASUREMENT_ENABLED | true |
| Custom pixel OPENAI_PURCHASE_ENABLED | true |
| Secrets, URLs and store domain | Keep existing values |
Do not expect 100% matching coverage across all events: anonymous page views and some guest purchases lack customer identifiers. Judge the new controlled purchase first. Historic warnings may persist until the reporting window changes.
7. Troubleshooting and rollback
| Outcome | Action |
|---|---|
| 503 / measurement_disabled | Expected while paused. Enable only for controlled tests or verified live operation. |
| 503 / Missing configuration | Check all four required names and Production deployment. |
| 401 / Invalid signature | Check SHOPIFY_WEBHOOK_SECRET against the Shopify Webhooks signing string. Do not use the OpenAI key. |
| 403 / Unexpected store | SHOPIFY_STORE_DOMAIN must exactly match sew-heart-felt.myshopify.com. |
| consent_missing_or_denied | Inspect cart-to-order attribute and the banner’s actual permission state. Never bypass the check to make an event appear. |
| non_web_or_cancelled_order_skipped | Sample or non-storefront source, or cancelled order. Test a controlled web checkout. |
| has_email false | Check SEND_HASHED_EMAIL and order email/contact_email availability. |
| has_external_id false | Check SEND_HASHED_EXTERNAL_ID and order.customer.id. Some orders do not expose a customer ID. |
| 502 / openai_rejected | Confirm key/pixel pairing and request schema. Developer must inspect validation response securely; this code intentionally does not log raw responses. |
| 503 / processing_failed | Developer checks runtime exception or timeout; do not log full orders or secrets. |
| Cart sync failure or checkout withdrawal failure | Keep live CAPI disabled; fix theme/checkout integration or server suppression first. |
Rollback: set MEASUREMENT_ENABLED false, custom pixel purchase switch false and theme measurement switch false. Save/deploy. Preserve backups. Do not restore an old consent-uncontrolled base script merely to recover tracking. Resolve the issue before reactivation.
The Worker has no durable queue. Shopify retries failed deliveries, but this does not guarantee recovery after retries expire. Monitor failures and add durable queuing/reconciliation for operational reliability.
Return these results to us
- Worker and theme installation confirmed; no secret values.
- Checkout and cookie-withdrawal tests passed, including Shop Pay and Buy it now if offered.
- Accepted and rejected test outcomes and matching booleans.
- New live purchase received, correct value/currency and deduplication confirmed.
- Final settings confirmed, or remaining blocker with server processing paused.
Prepared 14 October 2026 for Sew Heart Felt. Worker logic is checked locally with simulated signed webhooks; store, theme, checkout and OpenAI acceptance still require the tests above.
Official references: Shopify Customer Privacy API · Shopify Cart API · Pixel privacy API · OpenAI Conversions API · OpenAI Measurement Pixel.