Tags: analytics concept

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