Skip to content
Start free
Menu
Server API

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:

HTTP

Authorization: Bearer tl_sk_xxxxxxxxxxxxxxxxxxxxxxxxxx

The 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

JSON

{
  "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"
    }
  ]
}
FieldTypeNotes
v1The envelope version. A version the API does not know is rejected as unknown_version rather than guessed at.
eventIdUUID v7The idempotency key. Redelivering the same eventId never double-counts, so a retry after a timeout is always safe.
sessionIdUUID v7Create 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.
kindstringconversion, step, session_start, or error.
namestringThe name in the conversion path: lowercase, starting with a letter, then letters, digits or underscores, up to 64 characters.
occurredAtISO 8601When it happened, in UTC with millisecond precision. TraceLog assigns the event to this time, not to when it arrives.
identifierstringRequired on a conversion, 1 to 256 characters. Your own stable identifier.
valuenumberOptional, zero or greater.
currencystringOptional, ISO 4217 as three uppercase letters.
itemsarrayOptional, 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.
scheduledForstringOptional, on a conversion: the day it is for, YYYY-MM-DD.
recurringbooleanOptional, on a conversion: whether it recurs.
firstbooleanOptional, 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.
modestringOptional. verification marks the event as verification traffic, left out of metrics and session limits. Send it when your own test mode produced the order.
contextobjectOptional. 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

JSON

{ "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.

Command

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"
    }]
  }'
JavaScript

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

<?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);
Python

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
JSON

{ "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. Its refusal is payment_failure_invalid.
  • 401: the key is absent, unknown, revoked or not the server key.
  • 409: the project is on Shopify, whose checkout reports them. Its refusal is payment_failure_channel.
  • 429: too many requests with this key, the limit it shares with your events, with a Retry-After header in seconds.
  • 503: TraceLog could not store it.