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.
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 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
- 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.
- Deploy the template and fill in
META_PIXEL_ID,META_ACCESS_TOKENandALLOWED_ORIGINS(your site, for examplehttps://www.myshop.com). The dashboard password and API key are generated for you. - 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/_fbccookies to the relay, which improves matching. - Add the snippet below to your site and connect it to your cookie banner.
- Test. Set
META_TEST_EVENT_CODEto the code shown in Events Manager → Test events, openhttps://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' });
| Call | Effect |
|---|---|
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": []}
consentmust betruefor each event: you confirm the person consented.action_sourcedefaults towebsite; other Meta values (system_generated,physical_store,email…) are accepted. Website events needevent_source_url.event_time(Unix seconds) is optional and must be within the last 7 days.- Values already hashed with SHA-256 are passed through unchanged.
Endpoints
| Path | Purpose |
|---|---|
GET /capi.js | Browser helper |
POST /e | Browser events (CORS-restricted to ALLOWED_ORIGINS, rate-limited) |
POST /v1/events | Server events (Bearer API_KEY) |
GET /admin | Dashboard (HTTP basic auth, password ADMIN_PASSWORD) |
GET /healthz | Health check |
Configuration
| Variable | Required | Default | Description |
|---|---|---|---|
META_PIXEL_ID | yes | Dataset / Pixel ID from Events Manager. | |
META_ACCESS_TOKEN | yes | Conversions API access token from Events Manager. | |
ALLOWED_ORIGINS | yes | Comma-separated sites allowed to send browser events. | |
DATABASE_URL | yes | set by the template | Postgres connection string (private network). |
ADMIN_PASSWORD | yes | generated | Dashboard password. |
API_KEY | server API | generated | Bearer key for /v1/events. |
META_TEST_EVENT_CODE | no | Routes events to Events Manager → Test events. | |
REQUIRE_CONSENT | no | true | Set false only where the law does not require consent (not in the EU/UK). |
RETENTION_DAYS | no | 7 | Days before stored events are deleted (1–90). |
DEFAULT_PHONE_COUNTRY_CODE | no | Calling code added to national phone numbers, e.g. 33 for France. | |
META_API_VERSION | no | v26.0 | Graph 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:
- make sure your cookie banner calls
capi('consent', …)and that your Meta Pixel only loads after consent; - mention Meta's Conversions API in your privacy policy;
- respect Meta's Business Tools Terms (for example, do not send sensitive data such as health information).
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.