Trace Export
Send a trace of every finished request to your webhook, OpenTelemetry tool or Langfuse, and export the request log.
Send Traces to Your Tools
A trace describes one finished request: its model, provider, routing attempts, timings, token usage, cost and outcome. AudioLab sends a trace for each finished request to every enabled destination in the workspace, usually within seconds. Delivery happens after the request has finished, so a slow or failing destination never delays or fails your requests.
A destination is one of three types:
| Type | Sends | Use For |
|---|---|---|
| Webhook | Signed JSON batches | Your own service, queue or data warehouse |
| OpenTelemetry | OTLP/HTTP traces, JSON or Protobuf | Honeycomb, Grafana Cloud, an OpenTelemetry Collector |
| Langfuse | Langfuse ingestion events | Langfuse Cloud or a self-hosted Langfuse |
Traces carry metadata only unless a destination opts in to prompts and outputs. See Privacy Rules.
Add a Destination
Workspace owners, admins and developers can manage destinations. Viewers can see them.
- Open Traces in the dashboard and choose Add Destination.
- Enter a Name and choose a Type.
- Enter the Endpoint: a public
httpsURL on port 443, 4318 or 8443. For Langfuse, enter the Host, such ashttps://cloud.langfuse.com, with the Public Key and Secret Key. - Add any Headers the destination needs, such as an API key header. Header values, Langfuse keys and the webhook signing secret are stored encrypted and never shown again.
- Optionally limit Products and lower Sampling (%).
- Choose Add Destination. A webhook shows its signing secret once; copy it to your receiver.
- Choose Send Test from the destination's menu. A synthetic trace for the model
audiolab/testis sent at once, and the result shows the status your endpoint answered with.
Sampling is consistent: a request is always in or out for a given destination, and every request that sends the same session_id is sampled together. A workspace can have up to 10 destinations. AudioLab does not follow redirects, and refuses endpoints that resolve to private or internal addresses.
The same operations are available with a dashboard session through the Trace Export API.
What a Trace Contains
Every destination type carries the same fields. A webhook receives them as JSON, in batches of up to 50 traces. This is an illustrative excerpt of one webhook body:
{
"object": "list",
"data": [
{
"id": "lreq_example",
"object": "trace",
"workspace_id": "ws_example",
"key_id": "key_example",
"product": "llm",
"endpoint": "chat",
"model": "auto",
"resolved_model": "openai/gpt-5.4-nano",
"provider": "openai",
"status": "succeeded",
"finish_reason": "stop",
"error_code": null,
"routing": {
"mode": "auto",
"router": null,
"attempts": [
{
"model": "openai/gpt-5.4-nano",
"provider": "openai",
"role": "output",
"status": "succeeded",
"started_at": "2026-10-15T09:30:00.120Z",
"latency_ms": 840,
"ttft_ms": 310,
"failure_cause": null,
"upstream_status": null,
"finish_reason": "stop"
}
]
},
"usage": {
"input_tokens": 412,
"output_tokens": 96,
"reasoning_tokens": 0,
"cached_tokens": 0,
"input_chars": null,
"audio_seconds": null
},
"cost_usd": 0.000318,
"streamed": true,
"created_at": "2026-10-15T09:30:00.050Z",
"completed_at": "2026-10-15T09:30:01.010Z",
"duration_ms": 960,
"ttft_ms": 310,
"user": "user_1234",
"session_id": "chat_5678"
}
]
}routing, user and session_id apply to LLM requests, and routing.attempts lists every provider attempt, including failed ones with their failure_cause and upstream_status. Speech requests report input_chars and transcription requests report audio_seconds. When a destination includes prompts and outputs, an LLM trace also has content:
"content": {
"input": [{ "role": "user", "content": "Summarize this call." }],
"output": "The customer asked to move their delivery to Friday.",
"truncated": false
}Verify Webhook Signatures
Each webhook request has two headers:
| Header | Value |
|---|---|
AudioLab-Signature | t=<unix seconds>,v1=<signature> |
AudioLab-Delivery | An ID for the batch |
The signature is the hex HMAC-SHA256 of the timestamp, a period and the raw request body, keyed with the destination's signing secret. Verify it against the raw bytes before parsing the JSON, and reject a timestamp more than five minutes old so a captured request cannot be replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifySignature(rawBody, header, secret) {
const fields = new Map((header ?? "").split(",").map((part) => part.split("=", 2)));
const timestamp = Number(fields.get("t"));
const signature = fields.get("v1") ?? "";
if (!Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
fields = dict(part.split("=", 1) for part in (header or "").split(",") if "=" in part)
try:
timestamp = int(fields["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields.get("v1", ""))This receiver uses the Node.js helper. It needs Node.js 20 or later and no packages; run it with AUDIOLAB_SIGNING_SECRET set, behind HTTPS on a public host:
import { createServer } from "node:http";
import { verifySignature } from "./verify.mjs";
const secret = process.env.AUDIOLAB_SIGNING_SECRET;
const seen = new Set(); // Use durable storage in production.
createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
const body = Buffer.concat(chunks);
if (!verifySignature(body, req.headers["audiolab-signature"], secret)) {
res.writeHead(401).end();
return;
}
for (const trace of JSON.parse(body).data) {
if (seen.has(trace.id)) continue;
seen.add(trace.id);
console.log(trace.id, trace.model, trace.status, trace.cost_usd);
}
res.writeHead(204).end();
});
}).listen(8080);A trace can arrive more than once, for example after a timeout, so deduplicate on its id. Answer with any 2xx status within 10 seconds. To change the secret, choose Rotate Secret; the old secret stops working at once.
Send to OpenTelemetry Tools
An OpenTelemetry destination sends OTLP/HTTP traces to the full traces URL you enter, ending in /v1/traces. Each request becomes a client span named for its operation and model, with a child span for each provider attempt. Spans use the OpenTelemetry Gen AI attributes, such as gen_ai.request.model, gen_ai.provider.name, gen_ai.usage.input_tokens and gen_ai.conversation.id, plus audiolab.* attributes for cost, routing and status. The resource's service.name is audiolab-router. With prompts and outputs on, they are sent as gen_ai.input.messages and gen_ai.output.messages.
| Tool | Endpoint | Header | Encoding |
|---|---|---|---|
| Honeycomb | https://api.honeycomb.io/v1/traces, or https://api.eu1.honeycomb.io/v1/traces in the EU | x-honeycomb-team: an ingest API key | JSON or Protobuf |
| Grafana Cloud | Your stack's OTLP endpoint followed by /v1/traces, such as https://otlp-gateway-prod-us-east-0.grafana.net/otlp/v1/traces | Authorization: the Basic … value from your stack's OpenTelemetry tile | Protobuf |
| Langfuse | https://cloud.langfuse.com/api/public/otel/v1/traces, or https://us.cloud.langfuse.com/api/public/otel/v1/traces | Authorization: Basic and the Base64 of public_key:secret_key | JSON or Protobuf |
Copy the endpoint and credentials from the tool's own setup page, since hosts differ by region. Honeycomb names the dataset after service.name. For any other backend, such as Datadog or New Relic, send to an OpenTelemetry Collector you run behind HTTPS and export from there.
Send to Langfuse
The Langfuse type uses Langfuse's ingestion API directly. Enter the host only, such as https://cloud.langfuse.com, https://us.cloud.langfuse.com or your self-hosted address, and the project's Public Key and Secret Key.
Each request becomes a Langfuse trace with one generation per provider attempt. The attempt that answered carries token usage and cost, and the trace carries the request's user and session_id, so Langfuse sessions and user views work without extra setup.
Privacy Rules
By default a trace holds metadata only: IDs, product, model, provider, routing attempts, usage, cost, status, timings, and the user and session_id the caller sent. No prompts, outputs, audio, images or files are sent.
Turn on Prompts and Outputs for a destination to add LLM text to its traces. Only a workspace owner or admin can turn it on. These rules always apply:
- Only Chat Completions, Responses and Messages text is sent. Images, audio and files appear as placeholders such as
[image], and tool calls as[tool call name]. Embeddings, speech, transcription and other products never include content. - No content is sent for a request under zero data retention: when any Zero Data Retention option is on in the workspace's or account's privacy settings, or when the request sets
provider.zdrtotrue. See provider preferences. - The input and the output are each cut at 64 KiB, and
truncatedis set totrue. - AudioLab does not keep content. It is held only until each destination has received it, for at most one hour, and then deleted. It never appears in the request log or in log exports.
- Turning the switch off stops content for traces still waiting to be delivered. Deleting a destination drops its waiting traces.
Treat a destination with prompts and outputs as a copy of your users' data, and apply your own retention rules there.
Delivery and Retries
A delivery succeeds when the destination answers with a 2xx status. A failed delivery is retried with growing delays, after about 15 seconds, 1, 4, 16 and 20 minutes, and a Retry-After header is honoured up to 20 minutes. Timeouts, 408, 429 and 5xx answers are retried; other 4xx answers, redirects and endpoints that are not allowed are not.
A trace that cannot be delivered after six attempts becomes a dead letter. The Traces page shows each destination's last delivery and its failed deliveries, which are kept for 7 days. A destination holds at most 10,000 waiting traces; newer traces are dropped beyond that until it catches up.
Export Request Logs
To download the request log, open Usage in the dashboard and choose Export. Pick a Start Date, End Date, Product and Format: CSV or JSON Lines. Dates are in UTC.
The same file is available from Export Request Logs with an API key that has requests.read:
curl --fail --show-error "$AUDIOLAB_API_URL/v1/logs/export?format=csv&task=llm&after=2026-10-01T00:00:00Z&before=2026-10-15T00:00:00Z" \
--header "Authorization: Bearer $AUDIOLAB_API_KEY" \
--output audiolab-logs.csvAn export holds the same metadata as the request log, oldest first. It spans at most 366 days and 100,000 requests; a larger range is refused before anything downloads, so choose a shorter range or one product. A workspace runs one export at a time.