ElevenLabs Agents
ElevenLabs Agents export every finished call as an OpenTelemetry trace in a post-call webhook. Point that webhook at Oodle and each voice or chat conversation arrives with its turns, LLM calls, tool calls, tokens and cost. There is no code to add to the agent.
Oodle is a managed observability platform for metrics, logs, traces and agent traces. It ingests OpenTelemetry natively, so the snippet on this page is the complete setup.
Each trace shows the full transcript, the token counts and cost of every call, the agent structure (runs, steps and tool calls) and Signals: labels for loops, rate limits, refusals and tool failures, detected as the trace arrives.
Sign up for free to get an instance ID and an API key, or see the Agent Observability overview first.
You will need:
OODLE_INSTANCE: your Oodle instance ID (ap1, us1)OODLE_API_KEY: an Oodle API key (ap1, us1)OTLP_ENDPOINT: your OTLP collector domain, shown on the tile
Open the ElevenLabs Agents tile on the ap1, us1 page to
get these filled in for you, or let an agent do the setup with
/oodle-onboarding set up the llm_observability_elevenlabs integration.
Python
Install
Install the ElevenLabs SDK. ElevenLabs runs the agent and exports each conversation as an OpenTelemetry trace, so there is nothing to instrument:
pip install elevenlabs
Instrument
Create a post-call webhook that sends each conversation as an OpenTelemetry trace, and attach it to your agent. Every call then arrives with its turns, LLM calls, tool calls, tokens and cost:
# setup_webhook.py: run once
import os
from elevenlabs import ElevenLabs
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
webhook = client.webhooks.create(
settings={
"auth_type": "hmac",
"name": "Oodle Agent Observability",
"webhook_url": "https://<OTLP_ENDPOINT>/v1/otlp/traces/elevenlabs",
# Sent with every delivery: Oodle authenticates with them.
"request_headers": {
"X-API-KEY": "<OODLE_API_KEY>",
"X-OODLE-INSTANCE": "<OODLE_INSTANCE>",
},
}
)
# Retry a failed delivery, so that no call is lost.
client.webhooks.update(
webhook.webhook_id,
name="Oodle Agent Observability",
is_disabled=False,
retry_enabled=True,
)
# Send every conversation of the agent as an OpenTelemetry trace.
# For all agents, pass the same webhooks to
# client.conversational_ai.settings.update(webhooks=...).
client.conversational_ai.agents.update(
os.environ["ELEVENLABS_AGENT_ID"],
platform_settings={
"workspace_overrides": {
"webhooks": {
"post_call_webhook_id": webhook.webhook_id,
"events": ["transcript"],
"transcript_format": "opentelemetry",
}
}
},
)
Environment
Set these for the setup script:
export ELEVENLABS_API_KEY=<your-elevenlabs-api-key>
export ELEVENLABS_AGENT_ID=<your-agent-id>
TypeScript / JavaScript
Install
Install the ElevenLabs SDK. ElevenLabs runs the agent and exports each conversation as an OpenTelemetry trace, so there is nothing to instrument:
npm install @elevenlabs/elevenlabs-js
Instrument
Create a post-call webhook that sends each conversation as an OpenTelemetry trace, and attach it to your agent. Every call then arrives with its turns, LLM calls, tool calls, tokens and cost:
// setup-webhook.ts: run once
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
const client = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
const webhook = await client.webhooks.create({
settings: {
authType: 'hmac',
name: 'Oodle Agent Observability',
webhookUrl: 'https://<OTLP_ENDPOINT>/v1/otlp/traces/elevenlabs',
// Sent with every delivery: Oodle authenticates with them.
requestHeaders: {
'X-API-KEY': '<OODLE_API_KEY>',
'X-OODLE-INSTANCE': '<OODLE_INSTANCE>',
},
},
});
// Retry a failed delivery, so that no call is lost.
await client.webhooks.update(webhook.webhookId, {
name: 'Oodle Agent Observability',
isDisabled: false,
retryEnabled: true,
});
// Send every conversation of the agent as an OpenTelemetry trace.
// For all agents, pass the same webhooks to
// client.conversationalAi.settings.update({ webhooks }).
await client.conversationalAi.agents.update(
process.env.ELEVENLABS_AGENT_ID!,
{
platformSettings: {
workspaceOverrides: {
webhooks: {
postCallWebhookId: webhook.webhookId,
events: ['transcript'],
transcriptFormat: 'opentelemetry',
},
},
},
}
);
Environment
Set these for the setup script:
export ELEVENLABS_API_KEY=<your-elevenlabs-api-key>
export ELEVENLABS_AGENT_ID=<your-agent-id>
Through your own service
To keep the Oodle API key out of ElevenLabs, or to redact the transcript before it leaves your network, point the webhook at a forwarder in your service instead. It checks the webhook signature and posts the trace to your OpenTelemetry Collector as it is, and the collector exports to Oodle as described on the OpenTelemetry page.
Python
pip install elevenlabs fastapi uvicorn requests
# setup_webhook.py: run once
import os
from elevenlabs import ElevenLabs
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
webhook = client.webhooks.create(
settings={
"auth_type": "hmac",
"name": "Oodle Agent Observability",
# Where you run forwarder.py (below), reachable from the internet.
"webhook_url": "https://<your-forwarder-host>/elevenlabs/post-call",
}
)
# Give this secret to the forwarder: it checks deliveries with it.
print("ELEVENLABS_WEBHOOK_SECRET=" + webhook.webhook_secret)
# Retry a failed delivery, so that no call is lost.
client.webhooks.update(
webhook.webhook_id,
name="Oodle Agent Observability",
is_disabled=False,
retry_enabled=True,
)
# Send every conversation of the agent as an OpenTelemetry trace.
# For all agents, pass the same webhooks to
# client.conversational_ai.settings.update(webhooks=...).
client.conversational_ai.agents.update(
os.environ["ELEVENLABS_AGENT_ID"],
platform_settings={
"workspace_overrides": {
"webhooks": {
"post_call_webhook_id": webhook.webhook_id,
"events": ["transcript"],
"transcript_format": "opentelemetry",
}
}
},
)
# forwarder.py: run it at the webhook URL above. It checks
# each delivery and posts the trace to your collector.
import os
import requests
from elevenlabs import ElevenLabs
from fastapi import FastAPI, HTTPException, Request
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
app = FastAPI()
@app.post("/elevenlabs/post-call")
async def post_call(request: Request):
body = (await request.body()).decode()
try:
event = client.webhooks.construct_event(
rawBody=body,
sig_header=request.headers.get("elevenlabs-signature", ""),
secret=os.environ["ELEVENLABS_WEBHOOK_SECRET"],
)
except Exception:
raise HTTPException(status_code=401)
if event["type"] == "post_call_transcription_otel":
# The trace is OTLP/JSON already: post it as it is.
response = requests.post(
"http://otel-collector:4318/v1/traces",
json=event["data"]["otlp_traces"],
timeout=30,
)
if not response.ok:
# ElevenLabs retries a delivery that fails.
raise HTTPException(status_code=502)
return {"ok": True}
TypeScript / JavaScript
npm install @elevenlabs/elevenlabs-js express
// setup-webhook.ts: run once
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
const client = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
const webhook = await client.webhooks.create({
settings: {
authType: 'hmac',
name: 'Oodle Agent Observability',
// Where you run forwarder.ts (below), reachable from the internet.
webhookUrl: 'https://<your-forwarder-host>/elevenlabs/post-call',
},
});
// Give this secret to the forwarder: it checks deliveries with it.
console.log(`ELEVENLABS_WEBHOOK_SECRET=${webhook.webhookSecret}`);
// Retry a failed delivery, so that no call is lost.
await client.webhooks.update(webhook.webhookId, {
name: 'Oodle Agent Observability',
isDisabled: false,
retryEnabled: true,
});
// Send every conversation of the agent as an OpenTelemetry trace.
// For all agents, pass the same webhooks to
// client.conversationalAi.settings.update({ webhooks }).
await client.conversationalAi.agents.update(
process.env.ELEVENLABS_AGENT_ID!,
{
platformSettings: {
workspaceOverrides: {
webhooks: {
postCallWebhookId: webhook.webhookId,
events: ['transcript'],
transcriptFormat: 'opentelemetry',
},
},
},
}
);
// forwarder.ts: run it at the webhook URL above. It checks
// each delivery and posts the trace to your collector.
import express from 'express';
import { ElevenLabsClient } from '@elevenlabs/elevenlabs-js';
const client = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
const app = express();
// The signature covers the raw body, so read it as text.
app.post('/elevenlabs/post-call', express.text({ type: '*/*' }), async (req, res) => {
let event;
try {
event = await client.webhooks.constructEvent(
req.body,
req.header('elevenlabs-signature') ?? '',
process.env.ELEVENLABS_WEBHOOK_SECRET!
);
} catch {
return res.status(401).end();
}
if (event.type === 'post_call_transcription_otel') {
// The trace is OTLP/JSON already: post it as it is.
const response = await fetch('http://otel-collector:4318/v1/traces', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event.data.otlp_traces),
});
// ElevenLabs retries a delivery that fails.
if (!response.ok) return res.status(502).end();
}
res.status(200).end();
});
app.listen(8787);
What each call becomes
ElevenLabs describes a conversation with elevenlabs.* attributes, and
Oodle maps them onto the GenAI conventions when the trace arrives:
| ElevenLabs span | In Oodle |
|---|---|
elevenlabs.conversation | The agent run, named after the agent, with the conversation id as the session |
elevenlabs.recv.agent_response with LLM usage | An LLM call: the model, input, output and cached tokens, and the cost ElevenLabs reports. Its input is the conversation up to that turn, tool results included |
elevenlabs.tool.<name> | A tool call, with its parameters and result |
elevenlabs.recv.user_transcript | The caller's words, shown in the transcript |
Each conversation is one trace, and the conversation id is its session,
so the Sessions view groups calls by conversation. Start a
conversation with a user id (the user_id option of the ElevenLabs
SDKs) to group calls by caller as well.
An agent that already has a post-call webhook
An agent has one post-call webhook. If yours already goes to your own
service, keep it and forward the trace from there: the forwarder above
does that for the OpenTelemetry format. For the JSON format, fetch the
trace of each conversation with the ElevenLabs SDK
(conversations.get(conversation_id, format="opentelemetry")) and post
its otlp_traces to https://<OTLP_ENDPOINT>/v1/traces with the two
Oodle headers. Send each conversation once: ElevenLabs makes new span
ids on every export.
Verify
Make a call to the agent, by phone, in the ElevenLabs dashboard or with the widget. ElevenLabs sends the trace when its analysis of the call is done, about a minute after the call ends. Then open ap1, us1 and click the call for the Transcript, the waterfall and the cost breakdown.
If nothing arrives, open the webhook in the ElevenLabs dashboard: a
failed delivery shows the status Oodle answered. A 400 means the
transcript format is not OpenTelemetry, and a 401 means the
instance or the key in the webhook headers is wrong.
Support
If you need assistance or have any questions, please reach out to us through:
- Email at [email protected]