API reference
Most people never call these endpoints directly: the Python SDK and OpenTelemetry call them for you. Use this page if you are writing your own client.
- Base URL:
https://ingest.ai-tally.com - Authentication: every request sends your API key as
Authorization: Bearer <key>. The key needswriteoradminscope. - Where this comes from: the details below are generated from the ai-tally gateway’s own code.
POST /v1/batches
Section titled “POST /v1/batches”POST https://ingest.ai-tally.com/v1/batches
The endpoint the Python SDK posts to. Idempotent on batch_id: resending a batch after a timeout or a 429/503 never double counts it.
Authentication: Authorization: Bearer $TALLY_KEY, a key withwrite or admin scope.
Headers
| Name | Required |
|---|---|
X-Ingest-Protocol | No |
Body (JSON): The ai-tally batch format (tally.wire.BatchRequest). Unknown fields are tolerated. With key auth the key decides the tenant, so tenant_id may be empty.
| Field | Type | Notes |
|---|---|---|
tenant_id (required) | string | |
batch_id | string | Idempotency key for the batch. |
sdk_version | string | |
client_send_ts_ns | integer | |
resource_spans | array | Flat span objects whose keys are gen_ai.* attributes. Prompt, completion and other body fields are never stored. |
business_events | array | |
identity_links | array | |
sampling | object |
Success body (200)
| Field | Type | Notes |
|---|---|---|
batch_id | string | |
status | "accepted" | "partial" | "rejected" | "retry" | |
accepted_spans | integer | |
partial_errors | array | |
server_hints | object | |
replayed | boolean |
Responses
| Status | Meaning |
|---|---|
200 | Accepted. status is accepted, or partial when some spans were rejected (see partial_errors). A batch whose batch_id was already processed returns the original result with replayed: true. |
400 | INVALID_SCHEMA: X-Ingest-Protocol names a protocol the gateway does not support. Supported: ingest-v1. |
401 | UNAUTHENTICATED: missing bearer token, or an invalid or revoked key. |
403 | FORBIDDEN_SCOPE: the key has read scope and cannot write spans. TENANT_MISMATCH: the body names a tenant the key is not bound to. |
422 | Every span in the batch was rejected (a BatchResponse with status: rejected and per-span partial_errors), or the body is malformed ({"detail": "..."}). |
429 | RATE_LIMITED, QUOTA_EXCEEDED: back off for Retry-After seconds and resend the same body with the same batch_id. Header: Retry-After. |
503 | Temporarily unable to accept the batch (status: retry). Includes IDEMPOTENCY_UNAVAILABLE. Resend the same body with the same batch_id; it is never double counted. Header: Retry-After. |
Each span is a flat object of gen_ai.* attributes. Give each batch its own batch_id: sending the same batch_id again returns the first result instead of counting the batch twice, which makes retries safe.
curl https://ingest.ai-tally.com/v1/batches \ -H "Authorization: Bearer $TALLY_KEY" \ -H "Content-Type: application/json" \ -d '{"tenant_id": "", "batch_id": "my-batch-0001", "resource_spans": [{ "trace_id": "t1", "span_id": "s1", "gen_ai.system": "openai", "gen_ai.operation.name": "chat", "gen_ai.request.model": "gpt-4o-mini", "gen_ai.usage.input_tokens": 12, "gen_ai.usage.output_tokens": 5}]}'import json, os, urllib.request
body = { "tenant_id": "", # your key decides the organization "batch_id": "my-batch-0001", "resource_spans": [{ "trace_id": "t1", "span_id": "s1", "gen_ai.system": "openai", "gen_ai.operation.name": "chat", "gen_ai.request.model": "gpt-4o-mini", "gen_ai.usage.input_tokens": 12, "gen_ai.usage.output_tokens": 5, }],}request = urllib.request.Request( "https://ingest.ai-tally.com/v1/batches", data=json.dumps(body).encode(), headers={"Authorization": f"Bearer {os.environ['TALLY_KEY']}", "Content-Type": "application/json"},)with urllib.request.urlopen(request) as response: print(json.load(response))POST /v1/otlp/traces
Section titled “POST /v1/otlp/traces”POST https://ingest.ai-tally.com/v1/otlp/traces
Accepts an OTLP ExportTraceServiceRequest encoded as JSON (OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json). Protobuf is not supported. Spans are grouped by gen_ai.operation.name.
Authentication: Authorization: Bearer $TALLY_KEY, a key withwrite or admin scope.
Body (JSON): OTLP ExportTraceServiceRequest (JSON encoding).
| Field | Type | Notes |
|---|---|---|
resourceSpans | array |
Success body (200)
| Field | Type | Notes |
|---|---|---|
batch_id | string | |
status | "accepted" | "partial" | "rejected" | "retry" | |
accepted_spans | integer | |
partial_errors | array | |
server_hints | object | |
replayed | boolean |
Responses
| Status | Meaning |
|---|---|
200 | Accepted. status is accepted, or partial when some spans were rejected (see partial_errors). A batch whose batch_id was already processed returns the original result with replayed: true. |
401 | UNAUTHENTICATED: missing bearer token, or an invalid or revoked key. |
403 | FORBIDDEN_SCOPE: the key has read scope and cannot write spans. TENANT_MISMATCH: the body names a tenant the key is not bound to. |
422 | Every span in the batch was rejected (a BatchResponse with status: rejected and per-span partial_errors), or the body is malformed ({"detail": "..."}). |
429 | RATE_LIMITED, QUOTA_EXCEEDED: back off for Retry-After seconds and resend the same body with the same batch_id. Header: Retry-After. |
503 | Temporarily unable to accept the batch (status: retry). Includes IDEMPOTENCY_UNAVAILABLE. Resend the same body with the same batch_id; it is never double counted. Header: Retry-After. |
curl https://ingest.ai-tally.com/v1/otlp/traces \ -H "Authorization: Bearer $TALLY_KEY" \ -H "Content-Type: application/json" \ -d '{"resourceSpans": [{"scopeSpans": [{"spans": [{ "traceId": "5b8efff798038103d269b633813fc60c", "spanId": "eee19b7ec3c1b174", "name": "chat", "startTimeUnixNano": "'"$(date +%s)000000000"'", "attributes": [ {"key": "gen_ai.system", "value": {"stringValue": "openai"}}, {"key": "gen_ai.operation.name", "value": {"stringValue": "chat"}}, {"key": "gen_ai.request.model", "value": {"stringValue": "gpt-4o-mini"}}, {"key": "gen_ai.usage.input_tokens", "value": {"intValue": "12"}}, {"key": "gen_ai.usage.output_tokens", "value": {"intValue": "5"}}]}]}]}]}'import json, os, time, urllib.request
def attr(key, value): kind = "intValue" if isinstance(value, int) else "stringValue" return {"key": key, "value": {kind: str(value)}}
body = {"resourceSpans": [{"scopeSpans": [{"spans": [{ "traceId": "5b8efff798038103d269b633813fc60c", "spanId": "eee19b7ec3c1b174", "name": "chat", "startTimeUnixNano": str(time.time_ns()), "attributes": [ attr("gen_ai.system", "openai"), attr("gen_ai.operation.name", "chat"), attr("gen_ai.request.model", "gpt-4o-mini"), attr("gen_ai.usage.input_tokens", 12), attr("gen_ai.usage.output_tokens", 5), ],}]}]}]}request = urllib.request.Request( "https://ingest.ai-tally.com/v1/otlp/traces", data=json.dumps(body).encode(), headers={"Authorization": f"Bearer {os.environ['TALLY_KEY']}", "Content-Type": "application/json"},)with urllib.request.urlopen(request) as response: print(json.load(response))GET /v1/tenant/hmac-key
Section titled “GET /v1/tenant/hmac-key”GET https://ingest.ai-tally.com/v1/tenant/hmac-key
Returns the active HMAC-SHA256 key the SDK uses to hash account ids before they leave your process. Always requires a key with write or admin scope. The response is a secret: do not log it.
Authentication: Authorization: Bearer $TALLY_KEY, a key withwrite or admin scope.
Success body (200)
| Field | Type | Notes |
|---|---|---|
tenant_id | string | |
key_version | string | Key version, like v1. |
key_material_b64 | string | Treat as a secret. |
algorithm | "HMAC-SHA256" |
Responses
| Status | Meaning |
|---|---|
200 | The active key and its version. |
401 | UNAUTHENTICATED: missing bearer token, or an invalid or revoked key. |
403 | FORBIDDEN_SCOPE: a read key cannot fetch key material. HMAC_EXPORT_DISABLED: key export is disabled for this organization. |
404 | No key material exists for this organization. Hash nothing and send spans unattributed rather than sending raw ids. |
503 | The key store could not be reached. Retryable; no key material was returned. |
The reply contains your organization’s secret hashing key. Keep it out of logs. Most apps never call this: tally.hash_account calls it for you.
curl https://ingest.ai-tally.com/v1/tenant/hmac-key \ -H "Authorization: Bearer $TALLY_KEY"import json, os, urllib.request
request = urllib.request.Request( "https://ingest.ai-tally.com/v1/tenant/hmac-key", headers={"Authorization": f"Bearer {os.environ['TALLY_KEY']}"},)with urllib.request.urlopen(request) as response: key = json.load(response) # a secret: do not print or log it- Sending from Python? The Python SDK handles batching, retries and hashing for you.