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
# 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.
| Field | Type | Notes |
|---|---|---|
model | string | Required. Drives pricing when the provider is unknown. |
provider | string | gen_ai.system. Inferred from the model name when omitted. |
name | string | Run name. Defaults to the model. |
agent, release | string | Shown on the run; used by the agents breakdown. |
messages | any | Chat-style messages. Also used to derive the session. |
input, output | any | The call's input and output when messages does not fit. |
usage | object | input_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_at | ISO 8601 with offset | Or send duration_ms alone. |
duration_ms | number | Used when timestamps are not supplied. |
session_id | string | Group turns of one conversation. |
user | { id, name?, email? } | The end user behind the call. |
tags | string[] | Filterable on the run. |
metadata | object | Arbitrary keys, shown on the run's Metadata tab. |
tools | array, up to 100 | One 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
{
"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:
| Status | Code | Cause |
|---|---|---|
401 | unauthenticated | Missing, malformed or revoked ingest key. |
413 | payload_too_large | The body exceeds the accepted size. |
422 | validation_failed | A field failed validation; errors[] names the paths. |
429 | rate_limited | Too many requests. Honour Retry-After. |
429 | quota_exceeded | Three 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:
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.