Skip to content

Log endpoint

Send one JSON object per model call from Python, Go, Ruby, PHP or anything that speaks HTTP.

On this page

Make your model call exactly as you do now, then send one JSON object describing it. The server assigns ids and timestamps, derives the session, redacts bodies and hands the result to the same pipeline the SDK feeds — so the run, its generation span and its tool spans look the same in the app.

Endpoint
https://api.tracehatch.com/api/v1/log
Credential
Authorization: Bearer th_live_… or th_test_…
Success
202 Accepted, processed asynchronously
Required field
model

Send a call

Python
# After your existing model call, send only its metadata.
import json, os, urllib.request

request = urllib.request.Request(
    "https://api.tracehatch.com/api/v1/log",
    data=json.dumps({
        "model": "your-existing-model",
        "provider": "your-provider",
        "usage": {"input_tokens": 12, "output_tokens": 4},
        "duration_ms": 850,
        "status": "ok",
    }).encode(),
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["TRACEHATCH_API_KEY"],
        "Content-Type": "application/json",
    },
)
urllib.request.urlopen(request, timeout=10)

The body

Only model is required. Everything else is optional, and what you leave out is simply not recorded.

FieldTypeNotes
modelstringRequired. Drives pricing when the provider is unknown.
providerstringgen_ai.system. Inferred from the model name when omitted.
namestringRun name. Defaults to the model.
agent, releasestringShown on the run; used by the agents breakdown.
messagesanyChat-style messages. Also used to derive the session.
input, outputanyThe call's input and output when messages does not fit.
usageobjectinput_tokens, output_tokens, cached_input_tokens, reasoning_tokens.
status"ok" | "error" | "cancelled"Defaults to ok.
error{ type?, message? }Recorded on the span and used for root-cause selection.
started_at, ended_atISO 8601 with offsetOr send duration_ms alone.
duration_msnumberUsed when timestamps are not supplied.
session_idstringGroup turns of one conversation.
user{ id, name?, email? }The end user behind the call.
tagsstring[]Filterable on the run.
metadataobjectArbitrary keys, shown on the run's Metadata tab.
toolsarray, up to 100One tool span each. See below.

Each entry in tools takes name (required), input, output, status (ok or error), error, and started_at/ended_at or duration_ms.

The response

202 AcceptedJSON
{
  "batch_id": "bat_01j9xk3m4np6q7r8s9t0v1w2x3",
  "trace_id": "run_01j9xk3m4np6q7r8s9t0v1w2x3",
  "span_id": "span_01j9xk3m4np6q7r8s9t0v1w2a2",
  "session_id": "auto-5d41402abc4b2a76b9719d911017c592"
}

trace_id is the run to open under Traces. 202 means the call was stored; processing is asynchronous, so the run appears a moment later.

A sampling object is present only while the workspace is above its monthly trace allowance — see Plans, quotas and retention.

Sessions without a session id

When session_id is omitted and messages are chat-style, the session is derived from the system prompt and the first user message, exactly as the SDK and the gateway do. Every later turn resends both, so the whole conversation lands in one session without you tracking an id.

Send session_id explicitly when you already have a conversation id — it is more reliable than any inference.

Errors

Every non-2xx response is application/problem+json:

StatusCodeCause
401unauthenticatedMissing, malformed or revoked ingest key.
413payload_too_largeThe body exceeds the accepted size.
422validation_failedA field failed validation; errors[] names the paths.
429rate_limitedToo many requests. Honour Retry-After.
429quota_exceededThree times the monthly allowance. Honour Retry-After.

See HTTP API for the full problem shape.

Check the connection first

A dependency-free connection check, using nothing but the standard library:

tracehatch-check.pyPython
import json, os, urllib.request

request = urllib.request.Request(
    os.environ["TRACEHATCH_LOG_URL"],
    data=json.dumps({
        "model": "connection-check",
        "name": "Tracehatch connection check",
        "status": "ok",
        "duration_ms": 0,
        "tags": ["setup-check"],
    }).encode(),
    method="POST",
    headers={
        "Authorization": "Bearer " + os.environ["TRACEHATCH_API_KEY"],
        "Content-Type": "application/json",
    },
)
with urllib.request.urlopen(request, timeout=10) as response:
    print("Accepted. Open this run in Tracehatch:", json.load(response)["trace_id"])

Set TRACEHATCH_LOG_URL to https://api.tracehatch.com/api/v1/log. As everywhere else, a delivered check proves the key and the connection — not that your application is recording. See Verify your setup.

What this cannot do

The log endpoint sees only what you send it. It cannot assemble streams, measure time to first token, pair a tool call in one request with its result in the next, or notice a provider's client-side retry. If you want those without writing them yourself, use the provider gateway — or the Node SDK if your server is Node.js.