Server API
Confirm a conversion from your backend with one HTTP request authenticated by your project's server key. No server SDK is needed.
Browser evidence can be lost: a tab closes at the moment of payment, an extension blocks the request, a network drops. Confirming the conversion from your backend covers that gap. Browser and server evidence of one outcome count as one conversion, because both carry the same stable identifier.
Send the conversion from your server with an HTTP request and your project's server key. There is no server SDK to install.
The endpoint
https://api.tracelog.io/v1/server/events
POST, Content-Type: application/json, and your project's server key in
the Authorization header as a bearer token:
Authorization: Bearer tl_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxThe server key is a secret. It is not bound to an origin allowlist, anyone holding it can write events to your project, and it must never reach a browser bundle. Rotate it from project settings; the value is shown once, at creation or rotation.
The envelope
{
"v": 1,
"events": [
{
"eventId": "019b9ae7-2c00-7000-8000-000000000001",
"sessionId": "019b9ae7-2c00-7000-8000-000000000002",
"kind": "conversion",
"name": "purchase",
"occurredAt": "2026-01-08T00:00:00.000Z",
"identifier": "wc-1042",
"value": 129.5,
"currency": "EUR"
}
]
}| Field | Type | Notes |
|---|---|---|
v | 1 | The envelope version. A version the API does not know is rejected as unknown_version rather than guessed at. |
eventId | UUID v7 | The idempotency key. Redelivering the same eventId never double-counts, so a retry after a timeout is always safe. |
sessionId | UUID v7 | The visit the conversion belongs to. Use the one TraceLog's capture code created in the browser when you have it; otherwise create one. |
kind | string | conversion, step, session_start, or error. |
name | string | The name in the tracking plan: lowercase, starting with a letter, then letters, digits or underscores, up to 64 characters. |
occurredAt | ISO 8601 | When it happened, in UTC with millisecond precision. TraceLog assigns the event to this time, not to when it arrives. |
identifier | string | Required on a conversion, 1 to 256 characters. Your own stable identifier. |
value | number | Optional, zero or greater. |
currency | string | Optional, ISO 4217 as three uppercase letters. |
mode | string | Optional. verification marks the event as verification traffic, left out of metrics and session limits. Send it when your own test mode produced the order. |
context | object | Optional. Any JSON-serialisable object, up to 8 KB serialised. |
A conversion event needs six fields: eventId, sessionId, kind, name,
occurredAt and identifier. Limits: at most 50 events per batch and 256 KB
per request. An event more than five minutes in the future is rejected; one
more than seven days old is accepted and flagged as late.
The response
{ "accepted": 1, "rejected": [] }accepted counts the events stored by that request. One invalid event rejects
the whole batch: nothing is stored, and rejected lists each eventId that
could be read with the batch's reason — unknown_version, invalid_schema or
future_time. An eventId or sessionId that is not a UUID v7 is
invalid_schema.
An event from more than 30 days back, a day TraceLog no longer keeps, is
expired: it is refused on its own and the rest of the batch is stored.
A request over 256 KB or 50 events is refused whole with HTTP 413 and no
receipt: that is too_large. Too many requests with one key get HTTP 429
and a Retry-After header, in seconds.
Examples
curl -X POST "$TRACELOG_API/v1/server/events" \
-H "Authorization: Bearer $TRACELOG_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{
"v": 1,
"events": [{
"eventId": "019b9ae7-2c00-7000-8000-000000000001",
"sessionId": "019b9ae7-2c00-7000-8000-000000000002",
"kind": "conversion",
"name": "purchase",
"occurredAt": "2026-01-08T00:00:00.000Z",
"identifier": "1042",
"value": 129.5,
"currency": "EUR"
}]
}'await fetch(`${process.env.TRACELOG_API}/v1/server/events`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRACELOG_SERVER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
v: 1,
events: [
{
eventId: order.eventId, // a UUID v7 minted once per order and stored with it
sessionId: order.sessionId,
kind: "conversion",
name: "purchase",
occurredAt: new Date().toISOString(),
identifier: order.number,
value: order.total,
currency: order.currency,
},
],
}),
});<?php
$response = wp_remote_post( $api . '/v1/server/events', array(
'headers' => array(
'Authorization' => 'Bearer ' . $server_key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( array(
'v' => 1,
'events' => array( array(
'eventId' => $order->get_meta( '_tracelog_event_id' ), // UUID v7 stored with the order
'sessionId' => $session_id,
'kind' => 'conversion',
'name' => 'purchase',
'occurredAt' => gmdate( 'Y-m-d\TH:i:s.000\Z' ),
'identifier' => (string) $order->get_order_number(),
'value' => (float) $order->get_total(),
'currency' => $order->get_currency(),
) ),
) ),
) );import json, os, urllib.request
request = urllib.request.Request(
f"{os.environ['TRACELOG_API']}/v1/server/events",
method="POST",
headers={
"Authorization": f"Bearer {os.environ['TRACELOG_SERVER_KEY']}",
"Content-Type": "application/json",
},
data=json.dumps({
"v": 1,
"events": [{
"eventId": order.tracelog_event_id, # UUID v7 stored with the order
"sessionId": session_id,
"kind": "conversion",
"name": "purchase",
"occurredAt": occurred_at,
"identifier": str(order.number),
"value": float(order.total),
"currency": order.currency,
}],
}).encode(),
)
urllib.request.urlopen(request)Retrying
Retry on a network error, a 5xx, or a 429 once its Retry-After has passed,
with the same eventId. The eventId is the idempotency key, so a duplicate
delivery is stored once and counted once. A new eventId on a retry would be
counted again.