/webhooks/calls endpoint as every
other source, tagged with X-Zelto-Provider: livekit so the calls show
up as LiveKit.
Mint a key
- Open Settings → Integrations → LiveKit and click Create API key.
- Give it a descriptive name (e.g.
livekit-prod-worker). - 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 asZELTO_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.
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
Callforward_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 insession.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:
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.externalIdidentifies 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’sagent.namenames 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 addingcall.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 tohttps://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:
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: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 transcriptstartSeconds / 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 theowner 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:
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.
Related
- Traces — the OpenTelemetry span-tree view of a call (set up in Stream traces above).
- Connect a voice provider — pick a path and verify ingestion.
- Custom & other providers — the canonical call shape this endpoint accepts.
- Conversations — where LiveKit calls land.
- API reference · MCP — read your data programmatically.

