CAPI Relay

Server-side tracking for Facebook & Instagram ads, with consent built in.

CAPI Relay is a small, open-source service that sends your website's conversions to the Meta Conversions API from your own server. It enforces consent, hashes personal data on arrival, deduplicates against the browser Pixel and retries failed deliveries. One service, one Postgres database, no Google Tag Manager.

MIT licensed Consent-first SHA-256 hashing Retries & dedup 1 dependency

Why server-side?

Browser pixels miss conversions: ad blockers, Safari's tracking limits and closed tabs all drop events before they reach Meta. Meta recommends sending the same events from a server through the Conversions API, with a shared event_id so each conversion is counted once. CAPI Relay is that server, ready to deploy.

Your websitePixel + capi.js
→
CAPI Relayconsent · hashing · queue
→
Conversions APIMeta

Your backend can also send events directly with POST /v1/events.

Consent is enforced

Nothing leaves the browser until your cookie banner grants consent. Events without consent are dropped and never stored.

Data hashed on arrival

Emails, phones, names and addresses are normalized and SHA-256 hashed following Meta's rules before touching the database.

Reliable delivery

Durable Postgres queue, exponential backoff, invalid events isolated so they never block a batch, 7-day expiry.

Counted once

The browser helper fires the Pixel and the relay with the same event ID; repeated event IDs are ignored.

A dashboard that explains

Delivery rate, queue, failures with Meta's own error messages, setup checklist and a one-click test event.

Short retention

Stored events are deleted after 7 days by default. Only hashed contact data is ever kept.

Quick start

  1. Get your IDs from Meta. In Events Manager, open your dataset (Pixel), then Settings. Copy the dataset ID. Under Conversions API, click Generate access token. No app review is needed.
  2. Deploy the template and fill in META_PIXEL_ID, META_ACCESS_TOKEN and ALLOWED_ORIGINS (your site, for example https://www.myshop.com). The dashboard password and API key are generated for you.
  3. Add a custom domain on the service, such as t.myshop.com (Railway → service → Settings → Networking). On a subdomain of your site, the browser sends the Pixel's _fbp/_fbc cookies to the relay, which improves matching.
  4. Add the snippet below to your site and connect it to your cookie banner.
  5. Test. Set META_TEST_EVENT_CODE to the code shown in Events Manager → Test events, open https://t.myshop.com/admin (any username, password = ADMIN_PASSWORD) and press Send a test event. Remove the variable when you go live.

Website snippet

Load it after your Meta Pixel (which should itself only load after consent):

<script>
  window.capi = window.capi || function () { (capi.q = capi.q || []).push(arguments) };
</script>
<script async src="https://t.myshop.com/capi.js"></script>

From your cookie banner's callback, when the visitor accepts marketing cookies:

capi('consent', true);   // and capi('consent', false) if they withdraw it

Then send events with capi('track', …) instead of fbq('track', …). It fires the Pixel and the relay with the same event ID:

capi('track', 'ViewContent', { content_ids: ['sku-42'], content_type: 'product' });

capi('track', 'Purchase',
  { value: 49.90, currency: 'EUR', order_id: '1042' },
  { userData: { em: 'jane@example.com', ph: '06 12 34 56 78' }, eventId: 'order-1042' });
CallEffect
capi('consent', true | false)Grants or withdraws consent. Default: not granted.
capi('track', name, customData?, options?)Sends an event and returns its event_id. Options: userData, eventId, pixel: false.
capi('config', { pixel: false })Never call fbq (if you fire the Pixel yourself with the same eventID).

The helper never sets cookies. It reads the _fbp and _fbc cookies created by the Pixel, and builds fbc from an fbclid URL parameter when needed.

Server API

Send events from your backend: order confirmations, CRM updates, offline sales. Up to 500 events per request.

curl -X POST https://t.myshop.com/v1/events \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [{
      "event_name": "Purchase",
      "event_id": "order-1042",
      "consent": true,
      "event_source_url": "https://www.myshop.com/checkout",
      "user_data": {
        "em": "jane@example.com",
        "client_ip_address": "203.0.113.7",
        "client_user_agent": "Mozilla/5.0 …"
      },
      "custom_data": { "value": 49.90, "currency": "EUR" }
    }]
  }'

Response: {"accepted": 1, "duplicates": 0, "skipped_no_consent": 0, "errors": []}

Endpoints

PathPurpose
GET /capi.jsBrowser helper
POST /eBrowser events (CORS-restricted to ALLOWED_ORIGINS, rate-limited)
POST /v1/eventsServer events (Bearer API_KEY)
GET /adminDashboard (HTTP basic auth, password ADMIN_PASSWORD)
GET /healthzHealth check

Configuration

VariableRequiredDefaultDescription
META_PIXEL_IDyesDataset / Pixel ID from Events Manager.
META_ACCESS_TOKENyesConversions API access token from Events Manager.
ALLOWED_ORIGINSyesComma-separated sites allowed to send browser events.
DATABASE_URLyesset by the templatePostgres connection string (private network).
ADMIN_PASSWORDyesgeneratedDashboard password.
API_KEYserver APIgeneratedBearer key for /v1/events.
META_TEST_EVENT_CODEnoRoutes events to Events Manager → Test events.
REQUIRE_CONSENTnotrueSet false only where the law does not require consent (not in the EU/UK).
RETENTION_DAYSno7Days before stored events are deleted (1–90).
DEFAULT_PHONE_COUNTRY_CODEnoCalling code added to national phone numbers, e.g. 33 for France.
META_API_VERSIONnov26.0Graph API version.

Privacy and your responsibilities

CAPI Relay runs on your own infrastructure; you are the controller of the data it processes. Under the GDPR and ePrivacy rules, server-side tracking does not remove the need for consent. Before going live:

Stored: the Meta-formatted event (hashed contact data, IP address, user agent, fbp/fbc, your custom_data), delivery status and Meta's error messages, for RETENTION_DAYS only. Never stored: events without consent, raw emails or phone numbers.

FAQ

Do I need a Meta app or App Review?

No. The Conversions API only needs a dataset (Pixel) ID and an access token generated in Events Manager.

Will conversions be counted twice?

No, as long as the browser and the server send the same event ID. The capi('track') helper does this for you, and the relay ignores repeated event IDs.

Does it replace my cookie banner?

No. It relies on your banner: call capi('consent', true) when the visitor accepts marketing cookies.

What does it cost to run?

One small Node.js service and a Postgres database on Railway, billed by usage. There are no per-event fees.

How do I get help?

Ask in the template's Railway Central Station thread or open an issue on GitHub.