Server-Side Conversion APIs
Date: 2026-08-17
Sending conversions to ad platforms from your server rather than from the browser pixel. It exists because the pixel now misses a large share of conversions, and it works by sending the platform hashed identifiers to match against their logged-in users — which is a different privacy proposition from a cookie, not an escape from one.
A server-side conversion API is an ad platform’s endpoint that accepts conversion events sent from your server, with hashed customer identifiers, as a complement to the browser pixel.
What it fixes
BROWSER PIXEL CONVERSION API
purchase → pixel fires → platform purchase → your server → platform
blocked by ad blockers (~20–35%) not blockable
blocked by tracking prevention not affected
lost to consent decline ← STILL BLOCKED. see below
lost to page abandon before fire server-side, so it always fires
lost to JS errors no browser involved
cookie lifetime capped not cookie-dependent
typical observed loss: 20–40% typical recovery: much of it
The gap it closes is real — and the recovered conversions matter more than the reporting, because ad platform bidding algorithms optimise against the conversions they receive. Feeding them 65% of your conversions means bidding tuned on a biased two-thirds — Modelled Conversions.
How matching works
The platform can’t see your customer, so you send hashed identifiers and they match against their own users.
const sha256 = v => crypto.createHash('sha256').update(v).digest('hex');
// normalise BEFORE hashing — this is where match rates are won and lost
const payload = {
event_name: 'Purchase',
event_time: Math.floor(order.createdAt / 1000),
event_id: order.id, // ← deduplication key. essential
action_source: 'website',
user_data: {
em: sha256(order.email.trim().toLowerCase()),
ph: sha256(order.phone.replace(/\D/g, '')), // digits only, with
// country code, no +
fn: sha256(order.firstName.trim().toLowerCase()),
ct: sha256(order.city.trim().toLowerCase().replace(/\s/g, '')),
// pass through, unhashed — these do the heaviest matching
client_ip_address: req.ip,
client_user_agent: req.get('user-agent'),
fbc: cookies._fbc, // the click ID from the ad click
fbp: cookies._fbp,
},
custom_data: { currency: 'GBP', value: order.totalPence / 100 },
};Normalisation is the whole game for match rate. Alex@Example.com and alex@example.com hash to completely different values, so trimming and lower-casing before hashing is not a detail — inconsistent normalisation is the most common cause of a match rate that’s half what it should be.
The click ID is worth more than every hashed field combined. It’s a direct, deterministic link to the ad click, so capture it from the URL on landing, persist it in a first-party cookie, and send it with the conversion. A conversion with a click ID matches; one without relies on probabilistic identity matching.
Deduplication
Most implementations run the pixel and the API together, so both must be sent with the same event ID or every purchase counts twice.
browser pixel → event_id: "ORD-88214" ┐
├─ platform keeps ONE
server API → event_id: "ORD-88214" ┘
✗ using a random ID per send → double counting, silently
✗ using order ID in one place and
a session ID in the other → double counting, silently
✓ the same stable order ID in both
Double counting shows up as suspiciously good ROAS, which nobody investigates. Verify deduplication explicitly in the platform’s event diagnostics rather than assuming it — Idempotency and Deduplication, Double Counting.
What it does not fix
The critical correction, because this is routinely misunderstood and sometimes mis-sold:
Server-side sending does not remove the need for consent. The lawful basis attaches to sharing a customer’s identifiable data with an advertising platform, not to which machine sent it. If a user declined marketing consent, you must not send their conversion server-side either.
A conversion API implementation that ignores consent state is a compliance failure with better delivery. The consent decision has to travel from the browser to your server and be checked before sending — Consent Management, Legitimate Interest vs Consent.
Also unfixed: the platform still reports on its own attribution model in its own window, so its numbers will still disagree with yours — Walled Garden Reporting, Attribution Windows.
Implementing it sanely
- Send from the order-confirmed event in your backend, not from a webhook chain that might fire twice — Webhooks
- Include the full value and currency, in major units, correctly. A pence/pounds error here misprices bidding by 100× — Revenue Metrics
- Send refunds and cancellations too, or the platform optimises towards customers who return everything
- Monitor the match rate as a first-class metric. A drop usually means a normalisation regression or a field that stopped being populated — Data Quality Monitoring
- Send the same conversion to each platform separately. There’s no shared standard; each has its own field names, hashing expectations and event schema
- Keep it in one place. A conversion-sending service, not the same logic duplicated across checkout, admin and batch jobs — Server-Side Tag Management is one way to centralise it
Where it interacts
- Client-Side vs Server-Side Tracking — the general split; this is the ad-platform-specific case with identity matching attached
- Server-Side Tag Management — a common host for this, though the two are separable
- Offline Conversion Imports — the same matching applied to outcomes that happen after the website: qualified leads, closed deals, refunds
- Pseudonymisation and Anonymisation — hashed email is pseudonymous, not anonymous, which is exactly why the matching works
- Incrementality Testing — better matching improves the platform’s reported conversions and tells you nothing about whether the ads caused them