Skip to main content

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.

New to Oodle?

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 spanIn Oodle
elevenlabs.conversationThe agent run, named after the agent, with the conversation id as the session
elevenlabs.recv.agent_response with LLM usageAn 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_transcriptThe 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: