Use this guide if you have a custom voice stack or a platform without a dedicated SDK — Otherwise see Connecting to Pipecat, Connecting to LiveKit, Connecting to Dograh.
What this is
Custom Integration lets you push completed call data (transcript + metadata) from your own voice stack into Tuner so Tuner can process the call and run your configured analyses. There are two ways to get that data in:Observability libraries
Python stacks. Attach observers to your STT, TTS, and LLM framework — they capture the timeline, timing, and latency automatically and send everything in one
flush() at call end. Start with the Quickstart.Direct API
Any language, any stack. Build the call timeline yourself and send it to the Create Call API after the call ends. Follow the Happy Path below.
The Contract
To make calls show up under the right agent, your integration must consistently map these identifiers:- workspace_id: the Tuner workspace receiving the call
- agent_remote_identifier: Your Agent ID configured in Tuner. Get this value from Agent Settings > Agent Connection > Agent Remote ID
- call_id: your unique identifier for the call (use the same value every retry)
- sip_call_id (inbound simulation only): the SIP
Call-IDfrom the inbound call, required to link inbound simulated calls back to their Tuner simulation row. See SIP for other platforms. - recipient (outbound simulation only): the SIP URI your system dialed, exactly as Tuner sent it, required to link outbound simulated calls back to their Tuner simulation row. See Outbound Simulation.
Authentication
All API requests use Bearer authentication. Send your Tuner API key in theAuthorization header (copy it from Workspace Settings → API Keys & Integrations):
Authorization: Bearer <YOUR_TUNER_API_KEY>
TUNER_API_KEY environment variable and send it for you.
Happy Path (direct API)
Building against the API directly — because your stack isn’t Python, or you want full control over the payload:- Create a Tuner Agent and select Custom API as the integration type.
- In your voice system, after the call ends, capture:
- the full structured conversation timeline (including agent and user messages, tool calls, and any node or state transitions)
- call start and end timestamps
- a publicly accessible recording URL
- any relevant operational or enrichment data (for example: cost data, call outcome, dynamic variables, or provider-native analysis)
- Call the Create Call endpoint with all data based on the API reference
- Open Tuner → Calls to see the processed results.
session.flush() sends it.
API Reference
See the full Create Call request/response schema and examples: → Create Call API ReferenceLibraries
For Python stacks, Tuner ships small, focused observability libraries — one per role in your voice pipeline. Each hooks into your provider’s native event system to capture transcript, timing, and latency; your audio loops and synthesis code stay untouched. They share one lifecycle: create aTunerSession per call, attach the observers that match your stack, and flush() at call end.
Attach only the observers that match your stack — each is independent. If we don’t ship an adapter for one of your providers, extend the base adapter for that role (see Custom providers on the Speech-to-Text and Text-to-Speech pages); everything still flows through the same session and single flush().
Quickstart
The session lifecycle, credentials, and every observer wired together — start here.
Speech-to-Text
tuner-stt-observer — Deepgram and Speechmatics adapters for user transcript and STT latency.Text-to-Speech
tuner-tts-observer — Cartesia adapter, agent transcript, TTS/e2e latency, word-accurate interruption capture.LangChain / LangGraph
tuner-langchain — node transitions, tool calls, and per-node LLM latency in the Tuner transcript pipeline.Webhooks
You can add a webhook URL to your agent under Agent Settings → Webhooks to receive Tuner events — a notification with a link to each call after it’s analyzed, and real-time alerts when they fire. See the Webhooks guide for setup and payload examples.Best practices (what keeps integrations stable)
- Make retries safe: Treat
call_idas immutable and retry the same request on failure. - Send the final transcript: If you only have partial transcripts, send only when final to avoid incomplete analysis.
- Flush in
finally: If you use the libraries, callsession.flush()in afinallyblock so dropped connections and errors don’t skip it. - Log request IDs + responses: You’ll want these when debugging ingestion issues.
Common issues
Calls don’t appear in Tuner
Usually one of:- wrong
workspace_id agent_remote_identifierdoesn’t match the agent mapping you intended- the call was sent but the transcript is empty/invalid
- (libraries)
session.flush()never ran — put it in afinallyblock
Unauthorized (401)
- Ensure you’re sending
Authorization: Bearer <API_KEY>(notX-API-Key). - (libraries) Ensure
TUNER_API_KEYis set in the environment where your agent runs.
Validation errors (4xx)
- Follow the API reference for required fields and types.
- If you’re unsure which field failed, start from the minimal example in the API reference and add fields incrementally.