Track conversions server-side
Post conversions directly from your backend for checkouts that never run the Spar script or never redirect back to your site.
Most sites can track conversions in the browser: a dataLayer purchase
push, window.spar.track(), or the Checkout success page setting under
Settings > Installation. Server-side
ingestion is for the cases none of those cover, namely a hosted checkout
that never returns to your domain, or a merchant who needs
refund-accurate, ad-blocker-proof numbers straight from their order system.
1. Generate a server key
Go to Settings > Installation and, in the Server API key card, click Generate key. This key is a secret, separate from the public embed key, and it authenticates requests to the conversions endpoint. The key is shown once, when you generate it; store it in your backend's secrets, never in client-side code. Rotate key replaces it, and the old key stops working immediately.
2. Persist the visitor id onto the order
Spar can only attribute a conversion to a visitor it already recognizes. Read the visitor id in the browser and save it onto the order at checkout creation:
const visitorId = window.spar.visitorId;
// Attach visitorId to the order/cart you're about to create,
// so it's available when the order is fulfilled.Without this handoff Spar has nothing to attribute the conversion to, so don't skip it.
3. Post the conversion on fulfillment
When the order is confirmed (a webhook, a fulfillment job, whatever fires
reliably in your stack), POST to your Spar serve origin (the same origin
your embed snippet's spar.js loads from, found on the Installation page).
curl -X POST "https://<your-serve-origin>/v1/conversions" \
-H "Authorization: Bearer <your-server-key>" \
-H "Content-Type: application/json" \
-d '{
"visitorId": "<the visitor id saved on the order>",
"event": "checkout_completed",
"sourceEventId": "<your order id>",
"value": 49.99,
"currency": "USD"
}'await fetch("https://<your-serve-origin>/v1/conversions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPAR_SERVER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
visitorId: order.sparVisitorId,
event: "checkout_completed",
sourceEventId: order.id,
value: order.total,
currency: order.currency,
}),
});A successful call returns { "attributedTests": n }, where n is the
number of running tests the visitor was exposed to that this conversion
counted toward. n can legitimately be 0 if the visitor wasn't exposed to
any test.
Request fields
| Field | Required | Notes |
|---|---|---|
visitorId | yes | window.spar.visitorId, persisted on the order in step 2 |
event | yes | One of product_added_to_cart, checkout_started, checkout_completed |
sourceEventId | yes | Your own idempotency key, see below |
value | no | Order total. Powers average order value; omit if unknown |
currency | no | ISO currency code (e.g. USD) |
url | no | The page URL associated with the event, for reference |
Idempotency
Use your own order id (or another value that stays the same across retries)
as sourceEventId. If the same sourceEventId arrives twice for the same
visitor and test, Spar keeps the first and ignores the repeat, even when two
webhook retries land at the same moment. sourceEventId must be 1 to 128
characters of letters, numbers, _, ., :, or -.
The event name isn't part of that check. If you post more than one event
type for the same order (for example checkout_started and
checkout_completed), give each call its own sourceEventId, such as the
order id with the event name on the end (order-123-checkout_started,
order-123-checkout_completed). If you reuse the same id across event types,
only the first one counts.
Late conversions
A conversion is still accepted and attributed up to 7 days after the test it's attributed to ends, so fulfillment can lag checkout by days without losing attribution. Post the conversion when fulfillment completes; there's no need to send it early.
Value and currency
value and currency are what let AOV and units-per-transaction objectives
report a verdict. A conversion posted without value still counts toward
conversion-rate objectives, but is excluded from any objective that needs an
order amount.
Last updated on