BakeBetter Apps·Explainer Hub
← All Explainers
CAPI Fan-Out Middleware — Technical Plan
nerve-pain-pink-salt / v1
Integration Plan / Cloudflare Workers

One ClickMagick conversion, fanned out to every Meta pixel.

A middleware that receives an outgoing webhook from ClickMagick and delivers a server-side Meta Conversions API event to N ad-account pixels — preserving fbp / fbc / IP / UA from the original bridge-page click so match quality stays high.

Nerve Pain / Pink Salt BuyGoods aff_id 1879 CF Workers + D1 CAPI v21 Broadcast routing
N
Meta pixels supported
12x / 24h
CM retry window
3 tables
D1 schema (clicks, conv, deliv)
~5 files
TypeScript Worker
01 / Context

Why this middleware exists

The offer runs paid Meta ads from multiple ad accounts against one affiliate offer. We don't own the vendor checkout, so only CAPI can feed each pixel a conversion signal.

The Nerve Pain / Pink Salt offer runs paid Meta ads from multiple ad accounts against the same affiliate offer (BuyGoods aff_id 1879, vendor theneurosalt.com). We don't own the checkout or thank-you page, so conversions only come back via BuyGoods → ClickMagick postback. Meta's server-side Conversions API (CAPI) is therefore the only realistic way to feed each pixel the conversion signal it needs for optimization and ROAS.

Goal (v1) One conversion event from ClickMagick, fanned out to every configured Meta pixel via CAPI, preserving fbclid / fbp / IP / UA from the original bridge-page click so match quality stays high.
Non-goals (v1) Browser-side Pixel on vendor thank-you page (we don't own it). Complex per-campaign routing (all pixels run the same offer, so broadcast is correct). Non-Meta destinations (TikTok / Google can reuse the same click store later).
02 / Architecture

End-to-end data flow

From ad impression to per-pixel CAPI delivery. Two write paths into the middleware: bridge-side click capture (gives us IP/UA/fbp) and CM webhook (gives us conversion + PII).
Step 1 — Meta Ad Click
User clicks Meta ad
Ad URL parameters field stamps utm_source/medium/campaign/term/content with Meta dynamic tokens. Meta also appends fbclid.
Step 2 — Bridge LP (organiclifestylerituals.com)
Three scripts fire in order
Meta Pixel(s): PageView → sets _fbp cookie, captures fbclid
cmc.js: mints cmc_vid, records click with UTMs
track.js (NEW): POST /click to middleware with { cmc_vid, fbclid, fbp, ua, utms, landing_url } — IP read from CF-Connecting-IP server-side
Step 3a — Cloak
Adspect /go.php
302 to affiliate URL with subid={p:cmcvid}. Safe page served inline to bots/reviewers.
Step 3b — Vendor checkout
User buys on theneurosalt.com
We don't control this page; no browser pixel possible here.
Step 4 — Existing postback (unchanged)
BuyGoods → ClickMagick
URL already configured: www.clkmg.com/api/s/post/?uid=180898&s1={SUBID}&amt={COMMISSION_AMOUNT}&ref={CONV_TYPE}&email_hash={EMAILHASH}. Pixel ID 101091.
Step 5 — NEW: ClickMagick Outgoing Webhook
CM fires GET request to our middleware
Configured per-project (Neuropathy). Fires on Sale, Upsell, Rebill, Refund. Carries [cmc_vid], [fbclid], buyer PII, UTMs, Sub-IDs. 12-retry / 24h on non-200.
Step 6 — Cloudflare Worker (capi-fanout)
Validate → dedup → lookup → fan-out
(1) Validate auth= shared secret
(2) Insert conversions row (UNIQUE constraint = idempotent)
(3) Lookup clicks row by cmc_vid
(4) For each enabled pixel: build CAPI payload, SHA-256 hash PII, POST to graph.facebook.com/v21.0/{dataset_id}/events
(5) Log delivery row, enqueue non-2xx for retry
(6) Return 200 to CM
Step 7 — Meta Conversions API (multiple pixels in parallel)
Pixel A · Pixel B · Pixel C · …
Each pixel's attribution engine filters — only the ad account whose fbclid/fbp generated the click will attribute the conversion. Match quality indicator verifies quality of user_data.
Why bridge-side click capture is still required CM's outgoing webhook carries [fbclid] + full buyer PII, but NOT client IP, user-agent, or _fbp. Those three are what drives Meta's match quality from "fair" to "great." The bridge is the only place to capture them. Bridge row + webhook row join on cmc_vid.
Why broadcast routing Same offer, different ad accounts. Each Meta ad account only attributes conversions whose fbclid/fbp it actually set on its own ad. Firing the same event to all pixels is correct; Meta's per-account attribution engine filters. Requires installing ALL pixel init snippets on the bridge page.
03 / ClickMagick

Outgoing Webhook — confirmed capabilities

Per KB articles 217 & 793 (user-facing docs). Per-project configuration — for MVP only the Neuropathy project needs a webhook.

Navigation path

Log into ClickMagick → Projects → Neuropathy (hid 3518144721) → open dashboard → Tools menu → Webhooks → Outgoing Webhooks → Add Endpoint.

Trigger config

Choose Conversion Type(s): Sale (required), Upsell (recommended), Rebill (recommended if subscription), Refund (optional). One endpoint can serve all types — middleware routes on [cmc_ref].

Full endpoint URL (to paste into CM)

ClickMagick Endpoint URL
https://capi.organiclifestylerituals.com/conversion ?vid=[cmc_vid]&project=[cmc_project]&ref=[cmc_ref]&amt=[cmc_amt]&ts=[timestamp] &fbclid=[fbclid]&gclid=[gclid]&ttclid=[ttclid] &em=[email]&fn=[firstname]&ln=[lastname]&ph=[phone] &ct=[city]&st=[state_prov]&zp=[postal_code]&country=[country] &src=[utm_source]&med=[utm_medium]&cmp=[utm_campaign]&trm=[utm_term]&cnt=[utm_content] &s1=[s1]&s2=[s2]&s3=[s3]&s4=[s4]&s5=[s5] &auth=<SHARED_SECRET>

Available replacement tokens

CM replaces [token] with the actual value at webhook send time. Use bracket syntax, NOT {token}.

Identifiers
[cmc_vid]Visitor ID — primary key
[cmc_tid]Custom ID passed via &cmc_tid= on LP URL
[cmc_project]Project name (e.g. "Neuropathy")
[cmc_ref]Event type (sale / upsell / rebill)
[cmc_amt]Sale amount
[timestamp]UTC: 2026-01-27 22:55:19+0000
UTMs & Sub-IDs
[utm_source]
[utm_medium]
[utm_campaign]
[utm_term]
[utm_content]
[s1] [s2] [s3] [s4] [s5]From postback
Buyer PII (hash before sending to Meta)
[email]
[firstname]
[lastname]
[phone]
[mobile]
[gender]
[date_of_birth]
[city]
[state_prov]
[postal_code]
[country]
Ad click IDs
[fbclid]Meta
[gclid]Google
[wbraid]Google iOS
[msclkid]Microsoft
[ttclid]TikTok
[epik]Pinterest
NOT available in webhook (captured on bridge instead)
client_ipCaptured on bridge via CF-Connecting-IP
user_agentCaptured on bridge from request header
_fbp cookieRead from document.cookie on bridge
referrerCaptured on bridge via document.referrer
Retry behavior CM auto-retries up to 12 times over 24 hours on non-200 responses. Logs visible in CM's webhook log. Our endpoint must be idempotent against this — dedup key (cmc_vid, event_type, amt, ts_bucket) handles it.
04 / Stack

Tech choices & justification

Cloudflare-native: one vendor, one CLI, no container ops. Free tier covers this load.
LayerChoiceWhy
RuntimeCloudflare WorkersEdge, free tier fits this load, native bindings
StorageD1 (SQLite)Structured queries for clicks + deliveries debug, single binding
SecretsWrangler SecretsPer-pixel access tokens never live in source or KV
ConfigWorkers KVPixel list JSON, editable without redeploy
RetriesCloudflare QueuesExponential backoff (30s / 5m / 30m), 3 attempts
FrameworkHonoLightweight Workers router, TS-native
Custom domaincapi.organiclifestylerituals.comNew CF-proxied subdomain pointing at Worker

Files to create

PathPurpose
capi-fanout/wrangler.tomlWorker config: D1, KV, Queue bindings, route
capi-fanout/src/index.tsHono router: /click, /conversion, /health, /admin/*
capi-fanout/src/capi.tsCAPI payload build + SHA-256 + POST
capi-fanout/src/storage.tsD1 wrappers (upsertClick, insertConversion, logDelivery)
capi-fanout/src/pixels.tsKV config loader, secret resolver
capi-fanout/src/queue.tsRetry consumer
capi-fanout/schema.sqlD1 DDL
public/track.js~1KB bridge-page script
lp{1,2,3}.html editsAdd <script src="/track.js" defer>
05 / Data

D1 schema

Three tables. Dedup key (cmc_vid, event_type, amt, ts_bucket) handles CM's 12-retry window without losing legitimate repeat events (rebills on later days live in different buckets).
clicks — one row per bridge-page visit
schema.sql
CREATE TABLE clicks ( cmc_vid TEXT PRIMARY KEY, fbclid TEXT, fbp TEXT, ip TEXT, ua TEXT, utm_source TEXT, utm_medium TEXT, utm_campaign TEXT, utm_term TEXT, utm_content TEXT, landing_url TEXT, referrer TEXT, created_at INTEGER NOT NULL -- unix ms (used as fbc creation_time) ); CREATE INDEX idx_clicks_created ON clicks(created_at);
conversions — one row per unique conversion (idempotent)
schema.sql
CREATE TABLE conversions ( id INTEGER PRIMARY KEY AUTOINCREMENT, cmc_vid TEXT NOT NULL, event_type TEXT NOT NULL, -- Purchase | Subscribe | Refund cmc_ref TEXT, -- raw ref from CM (for debug) amt REAL, currency TEXT DEFAULT 'USD', ts_bucket INTEGER NOT NULL, -- floor(timestamp / 60) - 1-min bucket for dedup email_hash TEXT, raw_payload TEXT NOT NULL, -- full query string for replay received_at INTEGER NOT NULL, UNIQUE(cmc_vid, event_type, amt, ts_bucket) );
deliveries — one row per pixel attempt per conversion
schema.sql
CREATE TABLE deliveries ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversion_id INTEGER NOT NULL, pixel_name TEXT NOT NULL, pixel_dataset_id TEXT NOT NULL, status TEXT NOT NULL, -- success | retrying | failed http_status INTEGER, attempts INTEGER DEFAULT 0, fbtrace_id TEXT, response_body TEXT, created_at INTEGER NOT NULL, last_attempt_at INTEGER, FOREIGN KEY (conversion_id) REFERENCES conversions(id) );

Pixel config (Workers KV key: pixels_config)

pixels_config.json
{ "pixels": [ { "name": "FB-AcctA-Nerve", "dataset_id": "1234567890", "token_secret": "PIXEL_ACCTA_TOKEN", "test_event_code": null, "enabled": true }, { "name": "FB-AcctB-Nerve", "dataset_id": "2234567890", "token_secret": "PIXEL_ACCTB_TOKEN", "test_event_code": null, "enabled": true } ], "event_map": { "sale": "Purchase", "initial": "Purchase", "frontend":"Purchase", "upsell": "Purchase", "rebill": "Subscribe", "refund": "Refund" } }

token_secret resolves to a Wrangler Secret name at runtime (wrangler secret put PIXEL_ACCTA_TOKEN). Tokens never live in KV or git.

06 / CAPI

Meta Conversions API payload

POST to graph.facebook.com/v21.0/{dataset_id}/events?access_token={token}. Strip undefined keys recursively before send — Meta rejects the whole batch if any field is malformed.
src/capi.ts — payload builder
// PII from webhook is raw - hash before sending to Meta const hash = async (v?: string) => v ? bytesToHex(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(v.trim().toLowerCase()))) : undefined; { data: [{ event_name: mapped_event, // via event_map event_time: Math.floor(ts_ms / 1000), // CM [timestamp] parsed event_id: `${cmc_vid}:${event_type}:${amt}`, // dedup anchor action_source: "website", event_source_url: click.landing_url, user_data: { // High-priority match (from bridge click row) fbp: click?.fbp, fbc: fbclid ? `fb.1.${click?.created_at ?? Date.now()}.${fbclid}` : undefined, client_ip_address: click?.ip, client_user_agent: click?.ua, // PII (from CM webhook, hashed before send) em: await hashArr(email), ph: await hashArr(normalizePhone(phone)), fn: await hashArr(firstname), ln: await hashArr(lastname), ct: await hashArr(city), st: await hashArr(state_prov), zp: await hashArr(postal_code?.slice(0,5)), country: await hashArr(country?.toLowerCase()), external_id: [await hash(cmc_vid)!] // stable cross-event join }, custom_data: { value: Number(amt), currency: currency || "USD", content_type: "product", content_ids: ["nerve-pain-pink-salt"] } }], ...(pixel.test_event_code ? { test_event_code: pixel.test_event_code } : {}) }
Match quality tiers Great = fbp + fbc + IP + UA + hashed em/fn/ln + country. Good = fbc + IP + UA + hashed em. Fair = PII only (no fbp/fbc/IP). Our design aims for Great on every event where bridge click row exists.
07 / Verification

5-phase rollout checklist

Your progress persists in browser localStorage. Tick items as you complete them — refresh-safe.

Phase 1 Skeleton ready (no Meta calls yet)

Phase 2 Click capture wired

Phase 3 CAPI delivery against test pixel

Phase 4 CM webhook live + production pixels

Phase 5 Retry & failure drill

08 / Resilience

Failure modes handled

Every unhappy path is either caught, retried, or logged to /admin/deliveries for manual review.
FailureResponse
CM webhook arrives before bridge /click wrote row (race / JS off) Conversion row still inserted. CAPI called with only PII from webhook (no fbp/IP/UA). Logged match_quality: fair.
Meta returns 5xx or network error Enqueue to Cloudflare Queue. 3 retries at 30s / 5m / 30m.
Meta returns 4xx (malformed payload) Log failed with fbtrace_id + response body. No retry (would fail again). Surface via /admin/deliveries?status=failed.
Duplicate webhook (any of CM's 12 retries) UNIQUE(cmc_vid, event_type, amt, ts_bucket) swallows it. Return 200 without re-firing CAPI.
Pixel token rotated wrangler secret put PIXEL_X_TOKEN + /admin/pixels PUT updates KV. Next conversion picks up new values.
Missing click row (user bought without visiting our bridge — rare) CAPI fires with only PII from webhook. Logged match_quality: fair.
CM webhook auth missing / wrong 401 returned. CM log shows failures. Fix secret + webhook URL.
09 / Open

Items to confirm during execution

Things we'll verify the first time we open the UI / receive a real conversion. None of these block starting Phase 1.
Q1 — CM "Test Webhook" button KB article describes retries and logs but doesn't confirm a single-shot test button. Verify when the UI is open.
Q2 — BuyGoods → CM PII passthrough Does BuyGoods forward buyer firstname / email / phone to CM so the outgoing webhook carries them? Verify in CM's webhook log after first real conversion. If PII empty, we still have fbp/fbc/IP/UA from the bridge plus hashed external_id = cmc_vid — still functional, lower match quality.
Q3 — DNS for capi.organiclifestylerituals.com Decide at deploy time: (a) migrate zone from Hostinger to Cloudflare (cleaner, enables Worker routes), or (b) CNAME from Hostinger to the Worker's .workers.dev URL.
Q4 — Pixel dataset IDs + access tokens Supplied by you at config time. Wire one pixel first, verify end-to-end, then add the rest.
Q5 — Per-pixel _fbp nuance _fbp is domain-scoped, so all pixels on the bridge share it. Confirm the bridge <head> includes init snippets for every ad-account pixel — otherwise pixels without a PageView won't attribute even with correct fbp/fbc on the CAPI call.