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 bycurrency— 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 sameevent_idis 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 browserpurchase.vid— the same id you set in the browser withsetVisitor(), 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_8841and8841are different orders. A numeric browserorder_id: 8841does 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_checkoutand apurchasecarrying the sameorder_idare 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
purchaseis 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-levelcurrency, then the site's currency.revenue(decimal, e.g.238.00) orrevenue_cents(integer minor units, e.g.23800) — the order total. When neither is sent, the top-levelvalueis 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), orrevenue_centsin 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.