Server API
Last updated Nov 13, 2026
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. When you install with a script tag or with npm, Installation and keys in your project gives you the request with your conversion path's conversions already filled in.
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 your site's addresses, anyone holding it can write events to your project, and it must never reach a browser bundle. Rotate it from the project's Installation and keys page; 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 | Create one and store it with the conversion. The capture code does not expose the browser's session, and a server's confirmation does not need it. |
kind | string | conversion, step, session_start, or error. |
name | string | The name in the conversion path: 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. |
items | array | Optional, on a conversion or a step: up to 100, each an id, a name, an optional category, a quantity from 1 and an optional unit price. |
scheduledFor | string | Optional, on a conversion: the day it is for, YYYY-MM-DD. |
recurring | boolean | Optional, on a conversion: whether it recurs. |
first | boolean | Optional, on a conversion: true when your records hold no earlier conversion of this kind by the same person, false when they do; omit it when you do not know. |
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.
A malformed items, scheduledFor, recurring or first is left out and the
event kept. When your server and the browser both send items for one
conversion, TraceLog keeps your server's if its occurredAt falls on the day
the conversion was made, in your project's time zone. An item whose id is
(other) is counted under Other, with the items past each day's 500.
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.
A day's figures take late events for 30 days. An event from more than 30 days
back 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
The examples read two variables: TRACELOG_API, set to https://api.tracelog.io, and
TRACELOG_SERVER_KEY, your project's server key.
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: [
{
// a UUID v7 minted once per order and stored with it. In Node,
// v7() from the uuid package makes one; crypto.randomUUID()
// makes a v4, which TraceLog refuses.
eventId: order.eventId,
sessionId: order.sessionId,
kind: "conversion",
name: "purchase",
occurredAt: new Date().toISOString(),
identifier: order.number,
value: order.total,
currency: order.currency,
},
],
}),
});<?php
// A UUID v7: Unix milliseconds, version, variant and random bits.
function uuid_v7()
{
$ms = (int) floor(microtime(true) * 1000);
$hex = str_pad(dechex($ms), 12, '0', STR_PAD_LEFT);
$hex .= bin2hex(random_bytes(10));
$hex[12] = '7';
$hex[16] = dechex(8 | (hexdec($hex[16]) & 3));
return preg_replace('/^(.{8})(.{4})(.{4})(.{4})/', '$1-$2-$3-$4-', $hex);
}
// Mint both once, when the order is created, and store them with it.
$order['tracelog_event_id'] = uuid_v7();
$order['tracelog_session_id'] = uuid_v7();
// Once the payment is confirmed:
$curl = curl_init(getenv('TRACELOG_API') . '/v1/server/events');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('TRACELOG_SERVER_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'v' => 1,
'events' => [[
'eventId' => $order['tracelog_event_id'],
'sessionId' => $order['tracelog_session_id'],
'kind' => 'conversion',
'name' => 'purchase',
'occurredAt' => gmdate('Y-m-d\TH:i:s.000\Z'),
'identifier' => (string) $order['number'],
'value' => (float) $order['total'],
'currency' => $order['currency'],
]],
]),
]);
$response = curl_exec($curl);
curl_close($curl);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": [{
# UUID v7 stored with the order
"eventId": order.tracelog_event_id,
"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.
Failed payments
When your backend sees a payment fail, it can tell TraceLog, under the same server
key, with a POST to:
https://api.tracelog.io/v1/server/payment-failure
{ "gateway": "redsys", "occurredAt": "2026-11-04T10:15:30.000Z" }gateway is the payment method's name, 1 to 100 characters once trimmed.
occurredAt can be at most 48 hours old and at most five minutes ahead. TraceLog
keeps a count per day and gateway and nothing else of the order, so send no order
number, no amount and no address. A card refused and retried counts twice.
204: stored.400: the body is not as above. Itsrefusalispayment_failure_invalid.401: the key is absent, unknown, revoked or not the server key.409: the project is on Shopify, whose checkout reports them. Itsrefusalispayment_failure_channel.429: too many requests with this key, the limit it shares with your events, with aRetry-Afterheader in seconds.503: TraceLog could not store it.