contentBase64, with contentType, byteLength and what ElevenLabs reports in its headers: requestId, traceId, characterCost (credits charged) and historyItemId (songId on music). Base64 is expensive in an agent’s context, so speech operations (textToSpeech, textToDialogue, voiceChanger) take includeContent: false to leave it out, and leave it out on their own above maxBytes (default 700000 bytes, about 43 s of 128 kbps mp3). contentOmitted then says why and note says how to get it: history.getAudio({ historyItemId }). The history item can take a few seconds to appear after the audio returns.
Sound effects, music, stems and isolation have no history item, so ElevenLabs keeps no copy. Their output always comes back in the result, up to 50 MB.
Calls here wait for the audio. A script on Corsair’s cloud has a 25 s wall and can return at most 1,000,000 bytes, and for what we timed, size is the first limit. Single runs on 2026-11-08: a 10 s sound effect took 2.1 s, 530 characters of speech 1.1 s with eleven_flash_v2_5 and 1.7 s with eleven_multilingual_v2, and 15, 30 and 60 s of music took 3.9, 4.6 and 6.9 s. Music’s default format is about 24 KB/s, so its base64 passes the cap at about 31 s of audio: the 30 s result was 960,972 bytes, the 60 s result 1,920,973. To stay under the cap, ask for a lower-bitrate outputFormat (mp3_22050_32 gave 4.05 KB/s in one 6 s run, so about 3 minutes fits), or pass contentBase64 to the next operation in the same script and return only the metadata. We did not time long transcription, isolation, voice changer or stems. A run the wall cuts off may still finish and bill. dubbing.create is the one long operation with a job: follow it with dubbing.get, or let get_job do it. Transcripts of long files can use speechToText.convert({ webhook: true }).
ElevenLabs has no idempotency keys, so a repeated generation bills again. By default the plugin retries a generation only when ElevenLabs refused it before running it: a 429 with rate_limit_exceeded, concurrent_limit_exceeded, system_busy or the older too_many_concurrent_requests, at most 3 times, waiting as Retry-After says. If Retry-After is over 30 s it doesn’t wait and you get the 429. It never retries a generation on a 5xx, a timeout, a network failure or any other 429; those errors say the run may have happened. Reads retry any 429 under the same rules, and a 5xx at most 2 times. A custom handler in errorHandlers can change all of this, including retrying a generation after a 5xx, which can bill twice.
Not covered, on purpose: Studio and projects, professional voice training, instant voice cloning from files (consent and verification concerns), websockets, realtime and Speech Engine, streaming variants (an operation returns one value), phone numbers and outbound calls, workspace admin, SMB, and Flows.
What you get:
- 50 typed API operations
- 8 incoming webhook event types
Setup
1
Install
2
Add the plugin
corsair.ts
multiTenancy: true, scope calls with corsair.withTenant(id). See Quick start for KEK + Hub keys, and Multi-tenancy for account isolation.3
Choose authentication
- API Key (Recommended)
No setup required yet. When you make your first request as a tenant, Corsair prompts for the API key.More: API Key
4
Connect a tenant
Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see Connect / OAuth.
Example API calls
Find a voiceWebhooks
Create a webhook in ElevenLabs under Developers, Webhooks, with HMAC auth, and save the secret it shows you as the account’swebhook_signature (or pass webhookSecret); deliveries are refused until it is set. Tick the events you want, and select the webhook in the agent’s settings for post-call events. Hand processWebhook the raw body text (await request.text()), not parsed JSON, because ElevenLabs signs the exact bytes it sent. Deliveries name no account, so with several tenants put ?tenantId=<id> on the webhook URL and pass Object.fromEntries(new URL(request.url).searchParams) as processWebhook’s fourth argument. Passed as a URLSearchParams, the query falls back to the default tenant. The signature covers the body but not the URL, so ?tenantId= only picks which tenant’s secret checks a delivery. When tenants share a secret, by storing the same one or through the webhookSecret option, anyone holding a signed delivery under 30 minutes old can resend it with another tenantId, and that tenant’s hooks run on it. Give each tenant its own webhook secret, or check in an after hook that the event’s ids belong to that tenant before acting on it. By then the plugin has logged the event under that tenant. A before hook runs ahead of the signature check, so it can receive an unsigned or forged delivery. Keep side effects in after. A delivery with a bad signature or a payload that fails validation comes back from processWebhook as response.success: false, so answer non-2xx for it. The same event can arrive more than once, from ElevenLabs’ retries or from someone resending a signed delivery within 30 minutes, so make hooks safe to run twice, for example by recording the conversation_id or request_id each one acted on.
What’s next
API reference
Every
elevenlabs.api.* operation with input and output types.Webhooks
Event paths, payloads, and
webhookHooks examples.Connect / OAuth
createLink, Hub delivery, and tenant connect flows.
Use with agents
Expose this plugin’s operations as MCP tools.