Skip to main content
LiveKit runs your voice agents as workers you host — there is no cloud API for Zelto to pull calls from, and LiveKit’s room webhooks don’t carry a transcript. So the LiveKit integration is push-based: your agent worker forwards each finished session to Zelto with an API key. It uses the same provider-agnostic /webhooks/calls endpoint as every other source, tagged with X-Zelto-Provider: livekit so the calls show up as LiveKit.

Mint a key

  1. Open Settings → Integrations → LiveKit and click Create API key.
  2. Give it a descriptive name (e.g. livekit-prod-worker).
  3. Copy the key once — Zelto only stores a hash. It is the same org-wide key used by the REST API and MCP server.

Forward a session

Set the key as ZELTO_API_KEY in your worker’s environment. At the end of each session, send its transcript to https://ingest.zelto.ai/webhooks/calls using the example below. Install aiohttp in your worker environment if it is not already available. Use a stable agent ID across sessions and a unique room or session ID for each call. Reuse the same call ID when retrying an upload.
The body is Zelto’s canonical call shape — the same one documented under Custom & other providers, so see that page (or the REST API reference) for the full field list. Roles must be user, assistant, system, or tool. Everything except call.externalId and an agent reference is optional; you can also send durationSeconds, recordingUrl, endedReason, cost, and a customer block. A successful upload returns { "received": true }.

Declare the version that handled the session

Call forward_to_zelto with the release label and actual system prompt captured for that session. For example, keep agent_id="support-agent" for both releases and use agent_version="support-2026-09-a" or "support-2026-09-b". The Python parameter maps to top-level version in the API payload. Change the label for prompt or pipeline changes, even if the prompt stays identical. Keep the original label when retrying or uploading after another release has deployed. Do not use a room ID as the version or change the agent ID for each release. The first processed call creates the declared version. Once calls from both releases are visible, they can be selected for an experiment on that agent. Version labels alone do not capture the full pipeline. See Agent versions for captured settings, automatic fallback limitations, and the verification checklist. Pass version_config to include the complete release context as versionConfig: LLM, STT, TTS model/voice, tools, workflows, or custom settings. It must be a JSON object of at most 64 KiB. You can also register this context beforehand in Agents → Versions → Register version or through POST /v1/agents/{id}/versions. See the configuration contract.

Forward tool calls

If your LiveKit agent invokes tools during a session, forward each one so it appears inline in the transcript, feeds Zelto’s AI analysis, and lands in the queryable tool-call traces. LiveKit records tools in session.history.items as function_call items (the invocation, with name / arguments / call_id) and function_call_output items (the result, keyed by the same call_id). Map each function_call to a tool turn carrying a structured toolCall, and pair it with its output by call_id:
Only toolCall.name is required; arguments and result accept any JSON (a JSON string is parsed for you), and status is success, error, or pending. The turn’s content can be empty when a toolCall is present — Zelto labels the turn with the tool name. Send build_turns(session) as transcript.turns in the POST above.

Verify the first call

Forward a session, then open Conversations — it shows up tagged as LiveKit within a few seconds. If it doesn’t, check the response your worker got from the POST — a { "received": true } body means Zelto accepted it, a 4xx returns the reason — and confirm the worker is sending the Authorization and X-Zelto-Provider: livekit headers. See Connect a voice provider.

Stable ids

  • agent.externalId identifies the agent. Zelto finds or creates a LiveKit agent for each distinct value, so use a stable id per agent (not per session). The first session’s agent.name names it.
  • call.externalId (the room or session id) is the dedup key. Re-posting the same id updates the call in place — refreshing the transcript or duration, or adding a missing recording — and never double-charges. Keep the same agent reference too. Send the session once when it ends, then re-send the full original payload if you enrich it.

Recordings vs. transcripts

This flow forwards the transcript your agent already has. Attach an audio recording by adding call.recordingUrl to that same payload. The recording can come from egress or your own recording service, including an S3 bucket.

Recording ready when the call ends

Include the URL in your normal request to https://ingest.zelto.ai/webhooks/calls, using the existing Authorization: Bearer $ZELTO_API_KEY, X-Zelto-Provider: livekit, and Content-Type: application/json headers. For example, the call object in an otherwise complete session payload could be:
Keep the agent reference, transcript, metadata, and release context in the surrounding payload. Use the call ID you already send to Zelto; the recording’s filename is not a new call ID.

Recording ready later

Send the session as usual when it ends. Once the recording upload finishes, copy the full original payload, add the URL, and POST it to the same endpoint with the same headers:
Preserve the original agent reference, call.externalId, transcript, metadata, and version context (version, systemPrompt, and versionConfig, when sent). Also retain the original call details, including customer, cost, duration, and end reason. A recording-only partial resend can clear omitted fields such as cost or end reason. Persist the original payload if your recording notification runs in a different job or process. The resend attaches the recording to the same conversation without another call charge. It fills a missing recording or replaces a provider URL that Zelto has not yet captured. To recover an expired or inaccessible URL, resend the full original payload with a fresh downloadable URL. Retrying after Zelto has captured the recording preserves the stored copy.

Playback and capture

Users can play, pause, and seek the recording in the conversation’s existing audio player. Send transcript startSeconds / endSeconds relative to the recording’s start for synchronized playback; adding audio does not generate or replace transcript timings. The URL must be downloadable by Zelto without interactive authentication. A presigned URL works too: send it after the upload completes and keep it valid until capture finishes. Prefer an audio content type such as audio/wav for WAV recordings. WAV URLs served as application/octet-stream are also captured without changing the audio bytes. A bare URL to a private S3 object is not sufficient: send a presigned GET URL instead. Access-denied and not-found responses receive bounded retries; a confirmed expired URL requires a fresh URL. Zelto cannot renew URLs signed by your storage service. Zelto queues capture asynchronously and re-hosts the recording when your organization’s recording-storage setting is enabled. If your organization has opted out, playback uses your URL, which must remain accessible and support byte ranges for seeking. This integration does not change that setting. A 200 response with { "received": true } confirms acceptance, not completed capture. Before sending recordings for all calls, verify one call in Conversations: it should remain a single conversation, retain its transcript and details, and support playback and seeking. If re-hosting is enabled, confirm capture completed with Zelto before expiring the source URL. If you only have audio and no transcript, use the REST API file-upload flow instead; attaching call.recordingUrl here does not request transcription.

Connection status

The LiveKit card shows Active once Zelto has ingested at least one LiveKit call. Each agent’s detail page shows when its last session arrived, so you can tell at a glance whether the worker is still forwarding.

Stream traces (OpenTelemetry)

The forwarding above feeds Conversations — the transcript-and-analysis view of a call. Separately, a LiveKit agent can stream OpenTelemetry traces into Traces: the per-call span tree (LLM, TTS, STT, tool, and your own spans) with latency, tokens, and cost. LiveKit auto-instruments each session — you only register an OTLP exporter pointed at Zelto’s ingest host. Create a write-only observability ingest key in Settings → Integrations → Observability (or Traces → Connect agent) — it needs the owner or admin role — then register the exporter at the start of your entrypoint, before session.start() — it must actually run, not just be defined:
OTLPSpanExporter() reads the standard OpenTelemetry environment variables, and the SDK appends /v1/traces to the endpoint for you:
Install the HTTP exporter: pip install opentelemetry-exporter-otlp-proto-http. This Python HTTP exporter sends http/protobuf, so keep that protocol setting (Zelto also accepts OTLP JSON from compatible exporters). gRPC (:4317) is not accepted. This is separate from the transcript forwarding above: traces use the write-only ingest key — not your ZELTO_API_KEY — and need no X-Zelto-Provider header. See Traces for what a trace holds and how to read it. Set ZELTO_AGENT_EXTERNAL_ID to the same stable agent.externalId used by forward_to_zelto, and use ctx.room.name as call.externalId. The external ID creates or reuses the LiveKit agent even when telemetry arrives first. Metadata adds the session reference to LiveKit’s spans; include session.id on your own spans too. If a session includes several agents, set the external identity on each span rather than sharing one resource identity. See Send traces to verify delivery, configure logs, and troubleshoot, and LiveKit’s documentation for its OpenTelemetry integration.