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. 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 generate a server API key. This is a real secret, separate from the public embed key, and it authenticates requests to the conversions endpoint. The raw key is shown once, at generation time; store it in your backend's secrets, not in client-side code. Rotating the key invalidates the old one 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 there's nothing to attribute the conversion to: it's a hard prerequisite, not an optional extra.
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 stable across retries) as
sourceEventId. A repeated POST with the same sourceEventId for the same
visitor is rejected by a database unique index, not an application-level
lookup, so concurrent webhook retries can't double-count a conversion.
sourceEventId must be 1–128 characters of letters, numbers, _, ., :,
or -.
The unique index is on (test, visitor, sourceEventId). It does not
include the event name. If you post more than one event type for the same
order (for example checkout_started and checkout_completed), each call
must carry a distinct sourceEventId, such as the order id suffixed with
the event name (order-123-checkout_started,
order-123-checkout_completed). Reusing the same order id across event
types silently drops the later ones.
Late conversions
A conversion is still accepted and attributed up to 7 days after the test it's attributed to ends. Fulfillment can lag checkout by days without losing attribution: post the conversion whenever fulfillment actually completes, not artificially 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