OpinlySDK

Linking server events to browser visitors

How an order recorded on your server finds the anonymous browser visit that earned it.

The browser knows where a visitor came from. Your server knows what they bought. Attribution is the business of connecting those two facts — and a server-side event that arrives without a visitor link can only ever tell you that a sale happened, never which campaign earned it.

Opinly is built to keep that link intact. This page is the contract: follow it and every server-recorded sale traces back to the ad click that started it.

Why send anything server-side at all

Track purchases in the browser and you'll lose some. A tab closes mid-redirect, an ad blocker eats the request, the payment settles hours later on a webhook when nobody's looking. For a revenue number, "mostly right" isn't right.

Server-side events don't have those problems. What they don't have is identity.

Client sideServer side
Knows the campaignYes — it saw the clickNo
Survives ad blockersNoYes
Fires when the tab is closedNoYes
Best forBehaviour, attributionMoney, anything that must be exact

The answer is to send both and let them merge. That's supported directly — see Sending both.

The join key

Every browser visitor has an anonymous ID. That ID is the join key, and getting it onto your server-side call is the whole job.

A server event finds its visitor in one of three ways, best first:

You pass anonId — exact

The event attaches to precisely that visitor. Full attribution, no guessing. Always prefer this.

You pass email — inferred

Opinly hashes it and looks for a visitor who identified with the same address. If one exists, the event inherits that visitor's ID and original campaign — so a purchase your server reports with nothing but an email still lands on the right visit.

If the same address has identified on several devices, the event inherits the campaign of the visitor record created most recently — the device whose first visit is newest, not the one they used last.

If nobody matches, it falls through to the next case.

You pass neither — unattributed

The event is still recorded, with real revenue, against a synthetic visitor. It counts toward your totals and shows up as direct. Nothing is lost except the attribution — which is the entire reason you're here.

Send at least one of anonId or email on every server-side call. Neither is enforced — the event will be accepted, and it will silently be worth less than it should be.

Getting the anonymous ID to your server

Read it in the browser and pass it along with a request you're already making. There's no magic here and that's the point — it's your data, moving through your own API.

// browser
await fetch('/api/checkout', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    cart,
    anonId: window.opinly.anonId,
  }),
})
// your server
import { createOpinlyClient } from '@opinly/backend'
const opinly = createOpinlyClient()

const order = await charge(req.body.cart)

await opinly.track('purchase', {
  value: order.total,
  currency: 'USD',
}, {
  externalEventId: order.id,
  anonId: req.body.anonId,
  email: order.email,
})

Sending both anonId and email is the belt-and-braces move: the ID attributes exactly, and the email is the fallback for when the ID never reached us — a blocked first-visit beacon, a lost value, or an order placed with no ID captured at all.

If a checkout spans a redirect (a hosted payment page, say), stash the anon ID on the order record when the checkout starts, and read it back when the webhook confirms payment. The ID is just a string — persist it wherever the order lives.

Sending both

You can safely record the same order from the browser and from your server. Give both calls the same externalEventId and they collapse into a single event.

The two ids must match exactly — same string, same case. order_123 and 123 are two different orders as far as dedup is concerned, and you'd count the revenue twice. Generate the id once, from your real order number, and send that same string from both sides. (If you use trackPurchase, its orderId is the dedup key — pass your order number there and use the same value as the browser's externalEventId.)

// browser, on the thank-you page
window.opinly.track('purchase', { value: 49.0, currency: 'USD' }, {
  externalEventId: 'order_123',
})
// server, when the payment settles
await opinly.track('purchase', { value: 49.0, currency: 'USD' }, {
  externalEventId: 'order_123',
  email: order.email,
})

One order, one event, best of both — and two rules decide the merge:

  • The server's revenue wins. Your backend is the authority on what was actually charged, so its figure overwrites whatever the browser reported.
  • A real anonymous ID always beats a synthetic one. If the browser got there first with a genuine ID, a later server event that couldn't identify the visitor will not overwrite it.

The net effect: the server can only ever improve attribution, never degrade it. That's the property to hold on to. It means you can add server-side tracking to a working pixel setup without risking the data you already have.

WooCommerce does this for you

If you connect WordPress from the dashboard, this is already wired up. WooCommerce sends each order to Opinly server-side, keyed on its own order ID, and it merges with the browser's version by exactly the rules above. You don't need to write any of the code on this page — see WordPress.

Checking it worked

Open Analytics → Conversions and look at the source breakdown for the order.

  • Attributed to a campaign → the join worked.
  • Attributed to direct → the event arrived without a usable anonId or email, or the email didn't match any identified visitor. The revenue is right; the attribution isn't.

A run of unexpected "direct" revenue almost always means the anon ID isn't surviving the trip to your backend. Log what you're sending and confirm it isn't undefined.