Voicera API documentation
Submit an English SRT, calculate the complete token cost, generate selected-voice narration in every requested language, and receive a signed event when each result is ready.
Base URL
https://voiceralabs.com/api/v1Start here
Use the convenience narration-batch workflow when another platform already has an English SRT. Voicera creates the internal project and transcript automatically.
Input
English SRT
JSON string or multipart .srt file
Targets
1 or more
Unique supported language tags
Limits
40,000 chars
Up to 5,000 SRT cues
Output
MP3 audio
Authenticated content URL
How tokens work
Creating a draft does not spend tokens. Starting reserves the aggregate cost for all languages. Successful languages settle their share; failed languages release theirs. If the complete balance is unavailable, no jobs are created.Authentication and scopes
API requests use workspace-scoped bearer keys. Create keys from Developer settings; the full key is shown once and must stay on your server.
Authorization: Bearer vca_live_your_secret_key
Accept: application/jsonA missing scope returns 401 unauthorized. Resources belonging to another workspace return 404, which prevents workspace enumeration.
| Scope | Allows |
|---|---|
| projects:read | Read projects and project metadata |
| projects:write | Create projects through the convenience API |
| assets:read | Read private asset metadata and content |
| assets:write | Create and upload source or transcript assets |
| transcriptions:read | Read transcription jobs and transcripts |
| transcriptions:write | Import SRT or start transcription |
| renders:read | Read narration batches, jobs, voices, and languages |
| renders:write | Create and start narration jobs |
| billing:read | Read wallet and token ledger information |
| billing:write | Create quotes and reserve tokens |
| webhooks:read | Read registered endpoints and delivery history |
| webhooks:write | Create, update, test, rotate, delete, and replay webhooks |
Complete quickstart
This is the shortest production flow from credentials to downloaded audio. Every command uses the same workspace API key.
Register a webhook endpoint
The URL must be public HTTPS. Private, loopback, link-local, credential-bearing, and redirect-only destinations are rejected.
curl -X POST https://voiceralabs.com/api/v1/webhook-endpoints \
-H "Authorization: Bearer $VOICERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Production narration events",
"url": "https://platform.example.com/webhooks/voicera"
}'{
"endpoint": {
"id": "7cf7ef50-b248-4a30-b3f0-b40dbab973b4",
"name": "Production narration events",
"url": "https://platform.example.com/webhooks/voicera",
"status": "active",
"secretVersion": 1
},
"secret": "whsec_COPY_THIS_VALUE_NOW"
}Save the signing secret
Thesecret value is returned only on creation or rotation. It is not returned by endpoint-listing calls.Choose a target language and voice
# Select a supported target language
curl "https://voiceralabs.com/api/v1/languages?capability=narration" \
-H "Authorization: Bearer $VOICERA_API_KEY"
# Select a speaking voice
curl "https://voiceralabs.com/api/v1/voices" \
-H "Authorization: Bearer $VOICERA_API_KEY"Use the exact language code and voice id returned by these catalogs. The narration language list has no arbitrary selection-count cap.
Create the draft and aggregate quote
curl -X POST https://voiceralabs.com/api/v1/narration-batches \
-H "Authorization: Bearer $VOICERA_API_KEY" \
-H "Idempotency-Key: create-order-1042" \
-H "Content-Type: application/json" \
-d '{
"title": "Product launch",
"srt": "1\n00:00:00,000 --> 00:00:03,000\nWelcome to our launch.",
"voiceId": "VOICE_ID",
"targetLanguages": ["fr", "es", "de"],
"webhookEndpointId": "WEBHOOK_ENDPOINT_ID",
"externalId": "order-1042"
}'Upload the SRT as multipart instead
curl -X POST https://voiceralabs.com/api/v1/narration-batches \
-H "Authorization: Bearer $VOICERA_API_KEY" \
-H "Idempotency-Key: create-order-1042" \
-F 'srt=@english-source.srt;type=application/x-subrip' \
-F 'metadata={"title":"Product launch","voiceId":"VOICE_ID",
"targetLanguages":["fr","es","de"],
"webhookEndpointId":"WEBHOOK_ENDPOINT_ID","externalId":"order-1042"}'{
"id": "132f0e6d-4e36-42d8-9d61-6d8ec9aa4da5",
"projectId": "a4fa6a85-5ed1-449d-95ad-14f2b802f524",
"transcriptId": "transcript_01",
"externalId": "order-1042",
"voiceId": "VOICE_ID",
"voiceName": "Selected voice",
"targetLanguages": ["de", "es", "fr"],
"status": "draft",
"quote": {
"id": "e8de05e7-b235-4eec-942f-6c90c29592ae",
"estimatedTokens": 30,
"breakdown": {
"characterCount": 842,
"targetLanguageCount": 3,
"tokensPerLanguage": 10
},
"sufficientBalance": true,
"availableTokens": 500,
"status": "open",
"expiresAt": "2026-08-25T10:15:00.000Z"
},
"jobs": [],
"expiresAt": "2026-08-26T10:00:00.000Z"
}Store batch.id and batch.quote.id. The draft expires after 24 hours. Its quote expires after 15 minutes.
Display the estimate and start the entire batch
Show quote.estimatedTokens, quote.availableTokens, and quote.sufficientBalance before starting.
curl -X POST https://voiceralabs.com/api/v1/narration-batches/$BATCH_ID/start \
-H "Authorization: Bearer $VOICERA_API_KEY" \
-H "Idempotency-Key: start-order-1042" \
-H "Content-Type: application/json" \
-d '{"quoteId":"e8de05e7-b235-4eec-942f-6c90c29592ae"}'A successful response contains one queued job for every target language. Poll the batch or wait for webhooks. Do not start jobs individually after this succeeds.
Receive webhooks or poll, then retrieve MP3
curl https://voiceralabs.com/api/v1/narration-batches/$BATCH_ID \
-H "Authorization: Bearer $VOICERA_API_KEY"
# Download a completed MP3 using the permanent contentUrl/r2Url returned by the job
curl "$CONTENT_URL" --output narration-fr.mp3Poll every two to five seconds only while the batch is queued or running. Stop at succeeded, failed, or partially_succeeded. Result URLs still require the API key.
Long narrations run as durable translation and speech chunks. The job stage moves through planning, translating, generating_speech, and finalizing. Queue delivery is retry-safe and may be repeated internally without creating another public job.
Full JavaScript server example
const baseUrl = "https://voiceralabs.com/api/v1";
const headers = {
Authorization: "Bearer " + process.env.VOICERA_API_KEY,
"Content-Type": "application/json",
};
const draftResponse = await fetch(baseUrl + "/narration-batches", {
method: "POST",
headers: { ...headers, "Idempotency-Key": "create-order-1042" },
body: JSON.stringify({
title: "Product launch", srt, voiceId,
targetLanguages: ["fr", "es", "de"],
webhookEndpointId, externalId: "order-1042",
}),
});
if (!draftResponse.ok) throw await draftResponse.json();
const batch = await draftResponse.json();
const startResponse = await fetch(
baseUrl + "/narration-batches/" + batch.id + "/start",
{
method: "POST",
headers: { ...headers, "Idempotency-Key": "start-order-1042" },
body: JSON.stringify({ quoteId: batch.quote.id }),
},
);
if (!startResponse.ok) throw await startResponse.json();
console.log(await startResponse.json());Audio and video transcription
Upload media bytes or import a public HTTPS URL, wait for its private asset to become ready, then quote and start the normal transcription workflow.
Audio
2 GB / 8 hours
Upload or public HTTPS URL
Video
100 MB / 10 minutes
durationSeconds is required
Import media from a URL
curl -X POST https://voiceralabs.com/api/v1/projects/$PROJECT_ID/assets/imports \
-H "Authorization: Bearer $VOICERA_API_KEY" \
-H "Idempotency-Key: import-episode-42" \
-H "Content-Type: application/json" \
-d '{"url":"https://cdn.example.com/episode.mp4","kind":"video","durationSeconds":418}'Poll the response's Location every two to five seconds. Continue only after status is succeeded and asset.status is uploaded.
Quote and start transcription
POST /api/v1/billing/quotes
{"action":"transcription","projectId":"PROJECT_ID","assetId":"ASSET_ID"}
POST /api/v1/projects/PROJECT_ID/transcription-jobs
{"assetId":"ASSET_ID","language":"en","quoteId":"QUOTE_ID"}Video audio is extracted into bounded M4A windows before speech recognition. The transcription response contains a status URL and eventually a transcriptVersionId.
Remote-source safety
URLs must use public HTTPS and return Content-Length or Content-Range. Voicera revalidates redirects and rejects private, loopback, link-local, credential-bearing, and non-HTTPS destinations.Narration batch reference
These endpoints provide the complete SRT-to-audio workflow without requiring callers to create projects, assets, or transcript versions manually.
POST/narration-batchesCreate a draft narration batch and aggregate quote. Requires Idempotency-Key.
/narration-batchesRequired scope: projects:write, assets:write, transcriptions:write, renders:write, billing:write, webhooks:read
| Field | Type | Presence | Description |
|---|---|---|---|
| title | string | Required | Project title, 1–200 characters |
| srt | string/file | Required | English SubRip content or multipart .srt file |
| voiceId | string | Required | ID from GET /voices |
| targetLanguages | string[] | Required | Unique codes from GET /languages |
| webhookEndpointId | UUID | Required | An active workspace endpoint |
| externalId | string | Optional | Your stable order or content identifier |
Responses: 201 new draft · 200 identical replay · 409 conflicting replay · 422 invalid SRT, voice, or limits
POST/narration-batches/{batchId}/startReserve the aggregate quote and queue every language. Requires a new Idempotency-Key.
/narration-batches/{batchId}/startRequired scope: renders:write, billing:write
| Field | Type | Presence | Description |
|---|---|---|---|
| quoteId | UUID | Required | The current quote ID returned by this draft |
Responses: 202 all jobs queued · 402 insufficient balance and zero jobs · 409 expired, mismatched, or finalized quote
POST/narration-batches/{batchId}/quoteCreate a new 15-minute quote for an unstarted, unexpired draft. No request body.
/narration-batches/{batchId}/quoteRequired scope: renders:write, billing:write
Responses: 201 replacement quote · 409 batch is started, expired, or not quotable
GET/narration-batches/{batchId}Read authoritative status, quote, language jobs, errors, and permanent public R2 output URLs.
/narration-batches/{batchId}Required scope: renders:read
Responses: 200 batch · 404 batch is not visible to this workspace
Idempotency and lifecycle
Creation and starting are separate idempotent operations. Use a different stable key for each operation and retain it until the operation receives a terminal response.
| Operation | Recommended key | Replay behavior |
|---|---|---|
| Create draft | create-{yourExternalId} | Same body returns the existing draft; changed body returns 409 |
| Start batch | start-{yourExternalId} | Same quote and key return the existing queued batch |
| Requote | Not required | Creates a quote only while the batch remains an unstarted draft |
Draft
SRT stored; quote available; no tokens reserved.
Queued / running
Aggregate tokens reserved; language jobs processing.
Terminal
Succeeded, failed, or partially succeeded; tokens settled or released.
Webhook events and verification
Each language emits one terminal event. After every requested language is terminal, the batch emits one summary event. Delivery is at least once, so persist and deduplicate by Webhook-Id.
Request headers
| Header | Meaning |
|---|---|
| Webhook-Id | Stable event UUID used for deduplication |
| Webhook-Timestamp | Unix timestamp in seconds used for replay protection |
| Webhook-Signature | v1= followed by the base64url HMAC-SHA256 digest |
| Content-Type | application/json |
import crypto from "node:crypto";
function verifyVoiceraWebhook(req, rawBody) {
const eventId = req.headers["webhook-id"];
const timestamp = req.headers["webhook-timestamp"];
const received = req.headers["webhook-signature"];
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!eventId || !timestamp || !received || age > 300) return false;
const message = eventId + "." + timestamp + "." + rawBody;
const digest = crypto
.createHmac("sha256", process.env.VOICERA_WEBHOOK_SECRET)
.update(message)
.digest("base64url");
const expected = Buffer.from("v1=" + digest);
const actual = Buffer.from(received);
return expected.length === actual.length &&
crypto.timingSafeEqual(expected, actual);
}Verify the raw bytes
Do not parse and reserialize JSON before verification. Build the signed message aseventId.timestamp.rawBody, verify it in constant time, reject timestamps older than five minutes, and only then parse the JSON.narration.job.succeededOne language completed and its reserved tokens were settled.
{
"id": "f24a37dd-0bb2-42da-83cf-ae0f88e44d32",
"eventId": "f24a37dd-0bb2-42da-83cf-ae0f88e44d32",
"schemaVersion": "1",
"type": "narration.job.succeeded",
"batchId": "132f0e6d-4e36-42d8-9d61-6d8ec9aa4da5",
"externalId": "order-1042",
"projectId": "PROJECT_ID",
"jobId": "JOB_ID",
"targetLanguage": "fr",
"voice": { "id": "VOICE_ID", "name": "Selected voice" },
"status": "succeeded",
"tokenSettlement": { "tokens": 10, "state": "settled" },
"result": {
"format": "mp3",
"contentType": "audio/mpeg",
"contentUrl": "https://pub-6a185115e6634825bad38997237ba97a.r2.dev/public/narrations/projects/PROJECT_ID/jobs/JOB_ID/output.mp3",
"r2Url": "https://pub-6a185115e6634825bad38997237ba97a.r2.dev/public/narrations/projects/PROJECT_ID/jobs/JOB_ID/output.mp3"
},
"error": null,
"completedAt": "2026-08-25T10:04:22.000Z"
}narration.job.failedOne language failed and its reserved token share was released.
{
"id": "3a7fedf3-e992-4aa6-8097-71466d88d7b1",
"eventId": "3a7fedf3-e992-4aa6-8097-71466d88d7b1",
"schemaVersion": "1",
"type": "narration.job.failed",
"batchId": "132f0e6d-4e36-42d8-9d61-6d8ec9aa4da5",
"externalId": "order-1042",
"projectId": "PROJECT_ID",
"jobId": "JOB_ID",
"targetLanguage": "de",
"status": "failed",
"tokenSettlement": { "tokens": 10, "state": "released" },
"result": null,
"error": {
"code": "translation_failed",
"message": "The narration could not be translated."
},
"completedAt": "2026-08-25T10:04:25.000Z"
}narration.batch.completedThe final aggregate outcome with counts and per-language summaries.
{
"id": "9cb0c9b0-86ca-4335-bf32-8e22ae827c19",
"eventId": "9cb0c9b0-86ca-4335-bf32-8e22ae827c19",
"schemaVersion": "1",
"type": "narration.batch.completed",
"batchId": "132f0e6d-4e36-42d8-9d61-6d8ec9aa4da5",
"externalId": "order-1042",
"projectId": "PROJECT_ID",
"status": "partially_succeeded",
"counts": { "requested": 3, "succeeded": 2, "failed": 1 },
"jobs": ["one complete summary object per language"],
"completedAt": "2026-08-25T10:04:25.000Z"
}Delivery policy
- Only an HTTP 2xx response acknowledges an event.
- Timeouts, network failures, and non-2xx responses retry with backoff and jitter for 24 hours.
- Redirects are not followed and each request has a bounded timeout.
- Manual replay keeps the original event ID so recipient deduplication stays correct.
- Webhook delivery failure never changes a successful narration job.
Webhook management reference
Endpoints can be managed from the dashboard or with a key carrying webhook scopes. URLs must use public HTTPS.
Webhook management
/webhook-endpointsList webhook endpoints
webhooks:read/webhook-endpointsRegister HTTPS destination and reveal secret
webhooks:write/webhook-endpoints/{endpointId}Change name, URL, or status
webhooks:write/webhook-endpoints/{endpointId}Soft-delete an endpoint
webhooks:write/webhook-endpoints/{endpointId}/rotate-secretRotate and reveal a new secret
webhooks:write/webhook-endpoints/{endpointId}/testQueue a signed test event
webhooks:write/webhook-endpoints/{endpointId}/eventsList delivery history
webhooks:read/webhook-events/{eventId}/replayReplay with the original event ID
webhooks:writeErrors and safe retries
Every JSON error has a stable code, human-readable message, and request ID. Log the request ID when contacting support.
{
"error": {
"code": "insufficient_tokens",
"message": "This action needs 30 tokens, but only 20 are available.",
"requestId": "a783f34d-3da3-42bc-b8cf-bb90fb529a32",
"details": { "requiredTokens": 30, "availableTokens": 20 }
}
}| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_narration_batch | Malformed input, invalid SRT, duplicate languages, or missing fields. |
| 400 | idempotency_key_required | The required Idempotency-Key header is missing or invalid. |
| 401 | unauthorized | The API key is missing, invalid, expired, revoked, or insufficiently scoped. |
| 402 | insufficient_tokens | The workspace cannot fund the full batch. No jobs are created. |
| 404 | webhook_endpoint_not_found | The endpoint is outside this workspace, missing, or disabled. |
| 404 | narration_batch_not_found | The batch is outside this workspace or does not exist. |
| 409 | idempotency_conflict | The same key was reused with different input. |
| 409 | invalid_quote | The quote expired, was finalized, or does not match the batch. |
| 422 | invalid_srt | No valid cues were parsed, or the transcript exceeds 5,000 cues. |
| 422 | transcript_too_long | Narration text exceeds 40,000 characters. |
| 422 | invalid_voice | The selected voice is unavailable. |
Retry guidance
Retry network errors,408, 429, and 5xx with exponential backoff while keeping the original idempotency key. Fix the request before retrying 400 or 422. Requote before retrying an expired quote.Complete endpoint directory
All paths below are relative to /api/v1. The high-level batch endpoints are recommended for SRT narration integrations; granular project endpoints remain available for advanced workflows.
Convenience narration batches
/narration-batchesCreate project, import English SRT, and quote the full batch
multiple write scopes/narration-batchesList workspace narration batches
renders:read/narration-batches/{batchId}Read quote, jobs, errors, and result URLs
renders:read/narration-batches/{batchId}/quoteReissue an expired draft quote
renders:write + billing:write/narration-batches/{batchId}/startReserve the total and queue every language
renders:write + billing:writeCatalogs
/languages?capability=narrationList supported narration languages
renders:read/voicesList selectable voices
renders:read/voices/{voiceId}/previewStream a private voice preview
renders:readWebhook management
/webhook-endpointsList webhook endpoints
webhooks:read/webhook-endpointsRegister HTTPS destination and reveal secret
webhooks:write/webhook-endpoints/{endpointId}Change name, URL, or status
webhooks:write/webhook-endpoints/{endpointId}Soft-delete an endpoint
webhooks:write/webhook-endpoints/{endpointId}/rotate-secretRotate and reveal a new secret
webhooks:write/webhook-endpoints/{endpointId}/testQueue a signed test event
webhooks:write/webhook-endpoints/{endpointId}/eventsList delivery history
webhooks:read/webhook-events/{eventId}/replayReplay with the original event ID
webhooks:writeAdvanced project workflow
/projectsList or create projects
projects:read / write/projects/{projectId}Retrieve one project
projects:read/projects/{projectId}/assetsList or reserve private uploads
assets:read / write/projects/{projectId}/assets/importsImport a public HTTPS audio/video URL
assets:write/projects/{projectId}/assets/imports/{importId}Read remote import progress
assets:read/projects/{projectId}/assets/{assetId}Retrieve asset metadata
assets:read/projects/{projectId}/assets/{assetId}/contentUpload or retrieve asset bytes
assets:write / read/projects/{projectId}/assets/{assetId}/completeVerify a direct upload
assets:write/projects/{projectId}/assets/{assetId}/multipartCreate or inspect resumable upload
assets:write/projects/{projectId}/assets/{assetId}/multipart/parts/{partNumber}Upload one multipart part
assets:write/projects/{projectId}/assets/{assetId}/multipart/completeComplete multipart upload
assets:write/projects/{projectId}/transcription-jobsStart quote-backed transcription
transcriptions:write/projects/{projectId}/transcription-jobs/{jobId}Read transcription progress
transcriptions:read/projects/{projectId}/transcriptsImport parsed transcript segments
transcriptions:write/projects/{projectId}/transcripts/latestRead latest transcript
transcriptions:read/projects/{projectId}/transcripts/{transcriptId}Read or version transcript edits
transcriptions:read / write/projects/{projectId}/narration-jobsList or create one narration
renders:read / write/projects/{projectId}/narration-jobs/batchCreate multiple quote-backed narrations
renders:write/projects/{projectId}/narration-jobs/{jobId}Read narration progress
renders:read/projects/{projectId}/narration-jobs/{jobId}/contentStream or download MP3
renders:read/billing/quotesCreate a 15-minute action quote
billing:write/billing/walletRead available and reserved tokens
billing:read/billing/transactionsRead immutable token ledger
billing:readAccount and token purchasing
/billing/packagesList available USD or NGN token packages
public/billing/checkoutInitialize token checkout
billing:write/api-keysList keys created by the signed-in user
browser session only/api-keysCreate and reveal a new workspace key
browser session only/api-keys/{apiKeyId}Revoke a key created by the signed-in user
browser session onlyMedia dubbing endpoints — paused for new integrations
/projects/{projectId}/render-jobsCreate a source-voice media render
renders:write/projects/{projectId}/render-jobs/{jobId}Read media-render progress
renders:read/projects/{projectId}/dub-tracksList completed media dub tracks
renders:read/projects/{projectId}/dub-tracks/{trackId}/contentStream or download a dub track
renders:readNeed every schema definition?
The machine-readable OpenAPI 3.1 contract is the source of truth for generated clients.
