Spardocs

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

FieldRequiredNotes
visitorIdyeswindow.spar.visitorId, persisted on the order in step 2
eventyesOne of product_added_to_cart, checkout_started, checkout_completed
sourceEventIdyesYour own idempotency key, see below
valuenoOrder total. Powers average order value; omit if unknown
currencynoISO currency code (e.g. USD)
urlnoThe 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

On this page