Skip to content
AdPixDocsSearch the docsEnglishOpen console

Server-side tracking

Some events never happen in a browser — the payment confirmation the gateway posts to your server, an automatic renewal, an order support enters by hand. Your backend sends those directly with a server key: same data model, same identity, same reports.

When to go server-side#

The browser tag sees what happens on the page. Part of your business reality happens off the page: the payment gateway posts the result to your server, a subscription renews overnight, a support agent enters an order by hand, or the customer closes the browser before the gateway's response arrives.

Subject Browser tag Server-side send
Page views, navigation, engagement Yes No
Add to cart, begin checkout Yes Possible, usually unnecessary
A purchase confirmed by the gateway Incomplete and unreliable Yes
Renewals, recurring invoices, refunds No Yes
An order entered by an operator No Yes
An event behind an ad blocker Lost Yes

Both paths write to the same event table and feed the same identity graph. You do not have to give up one for the other — but do not send one event down both, because the two paths use different IDs and you will get two independent rows.

Already have a module?

On WHMCS you do not need to write any code — the AdPix addon calls this same API. Get it from Admin → Integrations and CRM modules, and create the server key in the same place.

Create a server key#

A server key is bound to a property and is created from any data stream's panel.

1
Open Admin in the console.
2
Under Property settings, select Data streams (or Integrations and CRM modules — the keys card is in both).
3
In the Server keys card, pick the stream, give it a name, and tick the scopes it needs.
4
Select Create server key and put the sk_… value straight into your backend's secret store.
The secret is shown once

Only the key's eleven-character prefix is retained; the secret itself is stored hashed and cannot be recovered. If you lose it, create a new key and revoke the old one. Both creation and revocation are recorded in the audit log.

There are four scopes. Grant the smallest set that works:

Scope What it opens
events sending events and orders (/events and /events/batch)
identify linking an anonymous visitor to a known user (/identify)
read:attribution reading a user's live attribution or an order's snapshot
read:identity reading a user's identity graph (linked IDs, accumulated traits)

Creating a key requires editor level or above; listing keys needs only report-read access — but the list shows the prefix, never the secret.

A write key is not a server key

Every data stream also has a write key, meant for the browser path and Tag Gateway. The server-to-server API does not accept it. Only a key beginning with sk_ works here.

Send your first event#

The base address is https://api.adpix.io/api/v1/s2s. Every request carries two things: the key in the Authorization header and the property ID in the X-Sov-Site header.

curl -X POST https://api.adpix.io/api/v1/s2s/events \
  -H 'Authorization: Bearer sk_...' \
  -H 'X-Sov-Site: <property_id>' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "purchase",
    "event_id": "order_10482",
    "order_id": "10482",
    "email": "ali@example.com",
    "value": 4900000,
    "currency": "IRR",
    "properties": { "gateway": "saman" }
  }'

Before wiring everything up, call POST /api/v1/s2s/verify once. If the key and the property match, the response returns the property's name and timezone, the key's scopes, the attribution window length, and whether HMAC is required — exactly what you need to debug a configuration.

Header Required Notes
Authorization: Bearer sk_… Yes The server key. Missing or malformed returns a 401.
X-Sov-Site Yes The ID of the property the key was created for.
Content-Type: application/json Yes The body is read up to a maximum of one megabyte.
X-Sov-Timestamp With HMAC only Unix time in seconds.
X-Sov-Signature With HMAC only Signature in the form t=<ts>,v1=<hex>.

Of the body fields, only event and event_id are required. value and currency carry revenue (the currency code must be three uppercase letters), items builds the order lines, and anything else goes in properties to be available in reports as event parameters.

event_id — the guarantee an order is not counted twice#

This is the most important rule on the page. You choose the event_id, and it must stably identify that same event in your system — order_10482, not a fresh random number on each attempt.

AdPix keeps the first response for each event_id along with a fingerprint of the request body. After that:

Re-sending with… Result
the same event_id and the same body the previous response is returned with a 200; no new event is written
the same event_id and a different body a 409 with idempotency_conflict; nothing is written
a fresh event_id a new event

So a network retry, a re-run of a job queue and a restart mid-way are all safe. This is the only collection path in AdPix that offers that guarantee; the browser does not.

Orders and the attribution snapshot#

If you send an order_id (or name the event purchase, order_updated or order_refunded and include the order ID), AdPix builds and freezes an attribution snapshot for that order: channel, source, campaign, first and last touch, days to conversion and the touch count.

The snapshot's model is last non-direct: if the last touch is direct or empty, the first touch takes its place.

The snapshot is written once. A later update to the same order brings the value and status but leaves attribution untouched — so your report is not rewritten by the customer's later behaviour. To read it:

curl 'https://api.adpix.io/api/v1/s2s/attribution?order_id=10482' \
  -H 'Authorization: Bearer sk_...' \
  -H 'X-Sov-Site: <property_id>'

Batch sending#

For a backfill or an overnight queue, send POST /events/batch with a body of {"events":[…]}. The per-request cap is 500 events; more returns a 413.

The response is a results array, in input order, one result per event. One invalid event does not take down the rest — its own result carries the error and the others are accepted. So always read results; the request's overall status code is not enough.

HMAC signing#

If you ticked "require HMAC" when creating the key, every request must also carry a signature. The signing string is <timestamp>.<raw body> and the signing key is the sk_… secret itself:

X-Sov-Timestamp: 1767225600
X-Sov-Signature: t=1767225600,v1=<hex(HMAC-SHA256)>

A clock difference of more than 300 seconds from the server returns stale_timestamp — so keep your server's clock in sync. The signature is computed over the raw body bytes, not over re-serialised JSON; if you re-serialise the body, the signature will not match.

If you send the signature headers, they are verified even when the key does not require HMAC.

What these events look like in reports#

There are a few deliberate differences from browser events. Not knowing them looks like missing data.

  • They have no session. A server-side event is not a browser session and AdPix does not create one for it. If it did, every CRM order would become a new "session" and would understate conversion rate. The consequence: these events are not counted in session-based metrics (engagement rate, sessions per user), but they are fully present in event counts, key events, revenue and user attribution.
  • They have no page URL. Page-based reports do not show them.
  • Their attribution comes from that user's history. If you send first_touch or last_touch in context, that is used; otherwise AdPix falls back to the durable first touch it stored for that visitor from the browser path. This is why sending anonymous_id makes a difference: without it, the order is not connected to the campaign that actually produced it.
  • Geography and device come from context. Send context.ip and context.user_agent to populate country and device type; omit them and those stay empty, because your server's IP is not a substitute for the user's.
Event rules do not run on this path

Create-event and Modify-event rules are applied to browser events at collection time, and the server-to-server path does not go through them. Send the event with exactly the name and parameters you want to see in reports. If you have a Create-event rule for the payment success page and you also send that purchase from your server, two independent conversions are recorded; pick one of the two paths. The full explanation is in Create and Modify event rules.

Errors#

Every error returns JSON and always carries a request_id. Include it if you open a ticket.

Code Error Meaning
401 invalid_key the Authorization or X-Sov-Site header is missing, or the key is unknown or revoked
401 bad_signature / stale_timestamp the HMAC signature does not match, or the clock difference exceeds 300 seconds
403 property_mismatch the key was not created for this property
403 insufficient_scope the key lacks the scope this path requires
400 bad_request the body is not valid JSON
422 validation_error event or event_id is missing, or the currency code is not three uppercase letters
409 idempotency_conflict the same event_id was already recorded with a different body
413 too_many_events the batch holds more than 500 events
429 rate_limited the key's rate limit is exhausted; see the Retry-After header

The rate limit is counted against the key itself. If you have a heavy backfill job, create a separate key for it so it cannot block your live shop traffic.

The legacy /api/v1/track endpoint#

There is an older path that accepts the browser tag's event shape with a server key. It takes no property header, builds no attribution snapshot, and gives no guarantee about re-sending. It is kept only for older integrations; write new work against /api/v1/s2s/events.

The complete request and response shapes, for when you are writing your own client, are in the Server-to-server API reference.

Frequently asked questions#

When should I reach for server-side sending?

When the event does not happen in the browser, or cannot be trusted there — a payment confirmation the gateway posts to your server, renewals and recurring invoices, cancellations and refunds, and orders an operator enters from an admin panel. For page views and on-site behaviour, the browser tag is still the correct source.

If I send the same request twice, is the order counted twice?

No, as long as the event_id is the same. AdPix keeps the first send's response and returns it again with a 200 for the second, without creating a new event. If you send the same event_id with a different body you get a 409 and nothing is written.

Is a server key different from a stream's write key?

Yes, and they are not interchangeable. The write key is for the browser path and Tag Gateway; the server-to-server API accepts only a server key (sk_). Putting a write key in the Authorization header returns a 401.

Why don't server-side events show up in session-based reports?

Because a server-side event has no browser session, and AdPix deliberately does not fabricate one. If it did, every CRM order would become a new session and would distort conversion rate. These events are fully present in event counts, key events, revenue and user attribution; they are only absent from metrics whose unit is the session.

Build with the APIUnderstand where revenue comes from.
Was this page helpful?