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 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

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 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

On this page