Docs Signed URLs
Signed URLs
Keep the ElevenLabs API key on your server and hand the browser a short-lived WebSocket URL instead.
Why signed URLs
A public agent id can open a socket directly from the browser. That is convenient for demos and openly shareable agents, but it is not a good fit for a private agent or for production traffic where you want control over who can start a conversation.
A signed URL is minted on your server with the workspace API key and expires quickly. The browser receives only the URL. The key never enters the bundle, the HTML, the WebSocket handshake, or the network tab.
Route contract
The controller calls a same-origin route when you pass signed: true. The default
endpoint is /api/signed-url.
GET /api/signed-url?agent_id=agent_...Success response:
{ "signed_url": "wss://api.elevenlabs.io/v1/convai/conversation?agent_id=..." }Error response:
{ "error": { "message": "Human-readable explanation." } }| Status | Meaning |
|---|---|
200 | Signed URL created. |
400 | Missing or malformed agent id. |
404 | ElevenLabs could not find the agent. |
500 | ELEVENLABS_API_KEY is missing on the server. |
502 | Upstream rejection, unexpected payload, or network failure. |
503 | ElevenLabs is rate limiting the request. |
499 | The client cancelled the request. |
Every response sets Cache-Control: no-store. The route validates agent_ and seng_ ids before it calls ElevenLabs, and it never echoes the
API key.
Server route
The repository route at src/routes/api/signed-url/+server.ts is the reference implementation.
A condensed version looks like this:
import { json } from '@sveltejs/kit';import type { RequestHandler } from './$types';const ORIGIN = 'https://api.elevenlabs.io';const AGENT_ID = /^(agent|seng)_[A-Za-z0-9]+$/;export const prerender = false;export const GET: RequestHandler = async ({ url, fetch, request, setHeaders }) => { setHeaders({ 'cache-control': 'no-store' }); const agentId = url.searchParams.get('agent_id')?.trim() ?? ''; if (!AGENT_ID.test(agentId)) { return json({ error: { message: 'Invalid agent id.' } }, { status: 400 }); } const apiKey = process.env.ELEVENLABS_API_KEY?.trim(); if (!apiKey) { return json({ error: { message: 'Missing server key.' } }, { status: 500 }); } const upstream = new URL('/v1/convai/conversation/get-signed-url', ORIGIN); upstream.searchParams.set('agent_id', agentId); const response = await fetch(upstream, { headers: { accept: 'application/json', 'xi-api-key': apiKey }, signal: request.signal }); if (!response.ok) { return json({ error: { message: 'Signed URL request failed.' } }, { status: 502 }); } const body: unknown = await response.json(); const signedUrl = typeof body === 'object' && body !== null && 'signed_url' in body ? body.signed_url : null; if (typeof signedUrl !== 'string') { return json({ error: { message: 'Unexpected upstream response.' } }, { status: 502 }); } return json({ signed_url: signedUrl });};Client usage
Pass signed: true as the second argument. The controller appends the agent id to
your endpoint and fetches JSON with an Accept header.
const conversation = useConversation({ agentId: 'agent_your_public_id', signedUrlEndpoint: '/api/signed-url'});await conversation.startConversation('agent_your_public_id', { signed: true });An already-minted wss:// URL skips the exchange and connects directly:
await conversation.startConversation('wss://api.elevenlabs.io/v1/convai/conversation?agent_id=...');The signed URL request has a 12 second timeout. A failed exchange appears as signed-url-failed with a message from your route when one is available.
Security checklist
- Read
ELEVENLABS_API_KEYfromprocess.envinside the server route only. - Never use a
PUBLIC_*name for the key and never import the route into client code. - Set
Cache-Control: no-storeon success and error responses. - Validate the agent id shape before calling ElevenLabs.
- Add authentication or rate limiting when the endpoint is public. A route that mints URLs for arbitrary ids can spend your quota.
- Do not log the key, the signed URL, or full request headers.
- Keep the key out of static builds, preview deployments, and issue reports.
Static hosting
A static host has no server runtime, so it cannot mint signed URLs. The showcase handles this by
building with PUBLIC_HOSTING_MODE=static: signed mode is hidden and disabled, and
visitors use a public agent id or paste a signed URL they already have.
PUBLIC_ELEVENLABS_AGENT_ID=agent_examplePublicId bun run build:pagesThe static output is written to build. Set PAGES_BASE_PATH for a
GitHub project site, for example /svelte-elevenlabs, so links and assets resolve
under the repository path. The workflow supplies it automatically and the same build also works
at a domain root.
Read Troubleshooting if signed mode works locally but fails after deployment, or see the README for the full Pages workflow.