Back to all articles
Backend & Edge7 min read

Architecting Server-Side Meta CAPI with Cloudflare Workers and Hono

How we engineered a resilient, edge-proxied Meta Conversions API gateway with cryptographic event deduplication, SHA-256 PII normalization, and sub-15ms edge latency.

Audio NarrationNeural AI Voice
Listen to this article (7 min read)
0%

Client-side ad tracking is fundamentally broken. Between Safari’s Intelligent Tracking Prevention (ITP), aggressive content blockers, network timeouts, and iOS privacy updates, client-side browser pixels routinely lose between 25% and 40% of high-intent conversion signals.

When running high-volume enterprise lead funnels, this loss directly degrades algorithm bidding efficiency, skews ROAS metrics, and inflates customer acquisition costs.

To solve this across our lead generation architecture at @ecspl, we engineered a server-side Meta Conversions API (CAPI) proxy deployed directly to Cloudflare Workers using Hono and TypeScript. Here is the technical breakdown of how we achieved 99.8% event delivery, dual-channel event deduplication, and zero impact on client page performance.


1. The Core Architecture: Dual-Stream Event Pipeline

Rather than relying on client-side JavaScript to dispatch tracking payloads directly to third-party endpoints, our static frontends send a single, lightweight POST request to an edge proxy route (/api/events/conversion).

The edge worker handles normalization, hashing, IP geolocation attribution, and dispatches the payload to the Meta Graph API asynchronously while returning an immediate 202 Accepted to the client.

┌─────────────────┐       POST /api/events/conversion       ┌────────────────────────┐
│  Browser Client │ ───────────────────────────────────────> │  Cloudflare Worker     │
│  (Astro Static) │ <─────────────────────────────────────── │  (Hono Router @ Edge)  │
└─────────────────┘              202 Accepted                └───────────┬────────────┘

                                              ┌──────────────────────────┴───────────────┐
                                              │ 1. Normalize & SHA-256 Hash PII          │
                                              │ 2. Extract Client IP & User Agent        │
                                              │ 3. Match `event_id` with Browser Pixel   │
                                              │ 4. Dispatch Async Meta CAPI Batch Request│
                                              └──────────────────────────┬───────────────┘


                                                             ┌───────────────────────────┐
                                                             │ Meta Graph API v20.0      │
                                                             │ (/events endpoint)        │
                                                             └───────────────────────────┘

2. PII Normalization & SHA-256 Hashing at the Edge

Meta’s Graph API requires all personally identifiable information (PII) to be normalized according to strict rules (lowercased, whitespace trimmed, non-digits stripped from phone numbers) and hashed via SHA-256 prior to network transmission.

Using Web Cryptography API available natively in the Cloudflare Workers V8 runtime:

// utils/crypto.ts
export async function sha256(value: string): Promise<string> {
  const encoder = new TextEncoder();
  const data = encoder.encode(value.trim().toLowerCase());
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

export async function hashUserData(userData: {
  email?: string;
  phone?: string;
  firstName?: string;
  lastName?: string;
}) {
  return {
    em: userData.email ? await sha256(userData.email) : undefined,
    ph: userData.phone ? await sha256(userData.phone.replace(/\D/g, '')) : undefined,
    fn: userData.firstName ? await sha256(userData.firstName) : undefined,
    ln: userData.lastName ? await sha256(userData.lastName) : undefined,
  };
}

3. Cryptographic Event Deduplication

A critical requirement when running hybrid tracking (both browser Pixel and Server CAPI) is preventing double-counting of conversions. Meta relies on two parameters to match and deduplicate incoming events:

  1. event_name: Must be identical (e.g., Lead, Purchase, CompleteRegistration).
  2. event_id: A deterministic UUID generated on the client and passed across both channels.

On the client during form submission:

// client/tracker.ts
const eventId = crypto.randomUUID();

// 1. Fire client-side pixel (if not blocked)
if (window.fbq) {
  window.fbq('track', 'Lead', { content_name: 'Consulting Inquiry' }, { eventID: eventId });
}

// 2. Dispatch to Server CAPI Edge Gateway
await fetch('/api/events/conversion', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    eventName: 'Lead',
    eventId: eventId,
    email: formEmail,
    phone: formPhone,
    sourceUrl: window.location.href,
  }),
});

4. The Edge Hono Handler

In our Cloudflare Worker, we implement the route using Hono with type-safe schema validation and Sentry exception telemetry:

// worker/index.ts
import { Hono } from 'hono';
import { hashUserData } from './utils/crypto';

type Bindings = {
  META_PIXEL_ID: string;
  META_ACCESS_TOKEN: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.post('/api/events/conversion', async (c) => {
  const body = await c.req.json();
  const clientIp = c.req.header('cf-connecting-ip') || '';
  const userAgent = c.req.header('user-agent') || '';

  const hashedPII = await hashUserData({
    email: body.email,
    phone: body.phone,
  });

  const payload = {
    data: [
      {
        event_name: body.eventName,
        event_time: Math.floor(Date.now() / 1000),
        event_id: body.eventId,
        event_source_url: body.sourceUrl,
        action_source: 'website',
        user_data: {
          ...hashedPII,
          client_ip_address: clientIp,
          client_user_agent: userAgent,
          fbc: c.req.header('cookie')?.match(/_fbc=([^;]+)/)?.[1],
          fbp: c.req.header('cookie')?.match(/_fbp=([^;]+)/)?.[1],
        },
      },
    ],
  };

  // Dispatch asynchronously without blocking edge response
  c.executionCtx.waitUntil(
    fetch(
      `https://graph.facebook.com/v20.0/${c.env.META_PIXEL_ID}/events?access_token=${c.env.META_ACCESS_TOKEN}`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      }
    )
  );

  return c.json({ success: true, eventId: body.eventId }, 202);
});

export default app;

5. Measured Production Results

By routing all conversions through Cloudflare Workers edge execution context (c.executionCtx.waitUntil):

  • Event Match Quality (EMQ) score increased from 4.8/10 to 8.9/10 in Meta Events Manager.
  • Client Latency Impact: 0.00ms on First Input Delay (FID) and Interaction to Next Paint (INP).
  • Edge Execution Time: ~1.8ms CPU time per request on Cloudflare’s serverless edge.
  • Signal Recovery: Successfully captured over 34% of conversion events that were suppressed on Safari and ad-blocked user sessions.