Conversions & revenue

Ecommerce & revenue

Attribute real money to the channels and campaigns that earned it. Start in the browser; move to the server for revenue you can fully trust.

Quick start: browser purchase event

On your order-confirmation page, fire a purchase with a value:

window.servoki.track('purchase', {
  value: 49.90,
  currency: 'EUR',
  plan: 'pro'
});

That's enough to see revenue by source, campaign and landing page in the dashboard. The catch: it runs in the browser, so ad-blockers and a closed tab on the thank-you page can cost you conversions.

Track the whole purchase journey

Fire an event at each step a shopper takes. Then you can build a funnel in the dashboard — view_item → add_to_cart → begin_checkout → purchase — and see exactly where people drop off, with revenue at every stage. Use these standard names so events group cleanly:

1. Product viewed — someone lands on a product page:

window.servoki.track('view_item', {
  item: 'SKU-204',
  name: 'Merino runner',
  price: 119.00,
  currency: 'EUR'
});

2. Added to cart — the key engagement signal. Fire it on the click, not on a page load, so it counts even on single-page stores:

window.servoki.track('add_to_cart', {
  item: 'SKU-204',
  name: 'Merino runner',
  price: 119.00,
  quantity: 1,
  currency: 'EUR'
});

3. Checkout started — pass the cart total and item count so you can compare started vs. completed value:

window.servoki.track('begin_checkout', {
  value: 238.00,
  items: 2,
  currency: 'EUR'
});

4. Payment details added — an optional late-funnel step that catches drop-off at the payment form:

window.servoki.track('add_payment_info', {
  value: 238.00,
  method: 'card',
  currency: 'EUR'
});

5. Purchase — the same confirmation event from above, now the final funnel step. The value is summed as revenue once you set value as the goal or funnel's revenue property (the built-in Ecommerce template does this for you):

window.servoki.track('purchase', {
  order_id: 'order_8841',
  value: 238.00,
  items: 2,
  currency: 'EUR'
});

Cart abandonment, for free. Because each step is its own event, the gap between add_to_cart and purchase in your funnel is your abandonment rate — no extra setup. Apply the Ecommerce template (or build a funnel view_item → add_to_cart → begin_checkout → purchase) in Goals & funnels to chart it.

Recommended: confirm purchases server-side

The reliable way to count revenue is from your backend, after payment is actually captured. Send a server event carrying the same order number as the browser's order_id, so the order counts once even when both fire:

curl -X POST https://servoki.com/api/events/server \
  -H "authorization: Bearer $SERVOKI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "site": "{{site}}",
    "event_type": "purchase",
    "value": 238.00,
    "currency": "EUR",
    "event_id": "order_8841",
    "vid": "anon-abc123",
    "props": {
      "order_id": "order_8841",
      "items": [
        { "sku": "SKU-204", "name": "Merino runner", "quantity": 2, "revenue": 238.00 }
      ]
    }
  }'
  • value — a number, summed as revenue. Goals report revenue split by currency — different currencies are kept separate, never converted into one another.
  • currency — a 3-letter ISO code (e.g. EUR). Stored with the event, used to group revenue by currency, and forwarded to the Meta Conversions API.
  • event_id — your order id. Makes retries safe: a repeat with the same event_id is stored once.
  • props.order_id — the order number, as a string. It records the purchase as an order (revenue and items in the ecommerce reports) and is the key that pairs it with the browser purchase.
  • vid — the same id you set in the browser with setVisitor(), so the server conversion stitches to the visit that drove it.

How a purchase is counted once

Both events are stored — the dashboard's conversion accuracy and reliability reports still show the browser and server side by side, which is how you see what ad-blockers cost you. But goal conversions, revenue, goal charts and funnel steps count one conversion per order: a browser purchase with props.order_id and a server purchase whose props.order_id — or, without one, whose event_id — is the same order number are counted once.

  • The server wins the facts. The order's revenue, currency and day come from the server event (the browser's value is used only if the server event carries none).
  • The browser keeps the journey. The visitor, session and traffic source come from the browser visit, so the order stays attributed to the campaign that drove it and stays in your funnel — even when the server event isn't stitched with vid.
  • The order numbers must match exactly. order_8841 and 8841 are different orders. A numeric browser order_id: 8841 does pair with the server's "8841", but sending strings on both sides is safest.
  • An order only one side saw — an ad-blocked browser, a server call that never happened — is counted once from that side. Events without any order number are counted exactly as before, one conversion each.
  • Only events of the same type pair up: a begin_checkout and a purchase carrying the same order_id are still two conversions.
  • Filtered views count the browser side. Server events have no browser, OS or page, so a dimension filter on one of those excludes them — the browser purchase is then the only row left and counts with its own value.
  • Attribution models use the browser purchase's time and visitor for a paired order (the server event only decides its revenue and the day it lands on in goal charts).

Order payload fields

A server purchase (or refund) whose props carry an order_id is also recorded as an order. These are the fields read from props; anything else is kept on the event but ignored by the order:

  • order_id — required, a non-empty string (max 200 characters). A number is not accepted here: the event is still stored as a conversion, but not recorded as an order.
  • currency — optional 3-letter ISO code; falls back to the top-level currency, then the site's currency.
  • revenue (decimal, e.g. 238.00) or revenue_cents (integer minor units, e.g. 23800) — the order total. When neither is sent, the top-level value is used.
  • items — optional, up to 200 entries of { sku, name, quantity, revenue | revenue_cents }:
    • sku — required string.
    • name — optional string.
    • quantity — optional number, defaults to 1.
    • revenue — the line total as a decimal (quantity × unit price), or revenue_cents in minor units.

Unknown item keys are silently dropped — a price (unit price) key is not read, so send the line total as revenue. A payload that claims an order_id but is otherwise malformed (e.g. an item without a sku) is rejected with 400 invalid_order_payload.

Stitching browser → server

Set a visitor id you control in the browser before the first page view, through the queue stub placed ahead of the Servoki script tag:

<script>window.servoki=window.servoki||function(){(servoki.q=servoki.q||[]).push(arguments)};</script>
<script defer data-site="example.com" src="https://servoki.com/tracker.js"></script>
<script>servoki('setVisitor', 'anon-abc123');</script>

Why the stub matters: the page view is sent the moment the tracker runs. Calling window.servoki.setVisitor() after the script has loaded only tags later events, so the page view and your funnel events (view_item, begin_checkout, purchase) land on two different visitors — funnels break and visitor counts double. The stub queues the id and the tracker applies it before the first page view.

Page caches: if your pages are cached (WordPress page cache, a CDN), don’t print the visitor id into the HTML on the server — a cached page would hand one visitor’s id to everyone who gets that copy, merging strangers into one visitor. Read the id in the browser (e.g. from your own cookie) inside the inline script instead.

Then pass that same id as vid on the server event. Now the purchase is attributed to the original source/campaign, even though it was confirmed on your backend.

Best of both: fire the browser purchase for instant feedback and the server event for truth. A matching order number means it counts once.