Skip to content
Guides/Using the API

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:

TypeSendsUse For
WebhookSigned JSON batchesYour own service, queue or data warehouse
OpenTelemetryOTLP/HTTP traces, JSON or ProtobufHoneycomb, Grafana Cloud, an OpenTelemetry Collector
LangfuseLangfuse ingestion eventsLangfuse 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.

  1. Open Traces in the dashboard and choose Add Destination.
  2. Enter a Name and choose a Type.
  3. Enter the Endpoint: a public https URL on port 443, 4318 or 8443. For Langfuse, enter the Host, such as https://cloud.langfuse.com, with the Public Key and Secret Key.
  4. 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.
  5. Optionally limit Products and lower Sampling (%).
  6. Choose Add Destination. A webhook shows its signing secret once; copy it to your receiver.
  7. Choose Send Test from the destination's menu. A synthetic trace for the model audiolab/test is 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:

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 Excerpt
"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:

HeaderValue
AudioLab-Signaturet=<unix seconds>,v1=<signature>
AudioLab-DeliveryAn 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.

verify.mjs
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));
}
verify.py
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:

receiver.mjs
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.

ToolEndpointHeaderEncoding
Honeycombhttps://api.honeycomb.io/v1/traces, or https://api.eu1.honeycomb.io/v1/traces in the EUx-honeycomb-team: an ingest API keyJSON or Protobuf
Grafana CloudYour stack's OTLP endpoint followed by /v1/traces, such as https://otlp-gateway-prod-us-east-0.grafana.net/otlp/v1/tracesAuthorization: the Basic … value from your stack's OpenTelemetry tileProtobuf
Langfusehttps://cloud.langfuse.com/api/public/otel/v1/traces, or https://us.cloud.langfuse.com/api/public/otel/v1/tracesAuthorization: Basic and the Base64 of public_key:secret_keyJSON 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.zdr to true. See provider preferences.
  • The input and the output are each cut at 64 KiB, and truncated is set to true.
  • 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:

Terminal
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.csv

An 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.