API v1JSON over HTTPSAt-least-once webhooks

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/v1

Start 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.
01Register webhook
02Choose voice + languages
03Create draft + quote
04Confirm and start
05Receive events + fetch MP3

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.

HTTP
Authorization: Bearer vca_live_your_secret_key
Accept: application/json

A missing scope returns 401 unauthorized. Resources belonging to another workspace return 404, which prevents workspace enumeration.

ScopeAllows
projects:readRead projects and project metadata
projects:writeCreate projects through the convenience API
assets:readRead private asset metadata and content
assets:writeCreate and upload source or transcript assets
transcriptions:readRead transcription jobs and transcripts
transcriptions:writeImport SRT or start transcription
renders:readRead narration batches, jobs, voices, and languages
renders:writeCreate and start narration jobs
billing:readRead wallet and token ledger information
billing:writeCreate quotes and reserve tokens
webhooks:readRead registered endpoints and delivery history
webhooks:writeCreate, 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.

1

Register a webhook endpoint

The URL must be public HTTPS. Private, loopback, link-local, credential-bearing, and redirect-only destinations are rejected.

cURL
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"
  }'
Response: 201 Created
JSON
{
  "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

The secret value is returned only on creation or rotation. It is not returned by endpoint-listing calls.
2

Choose a target language and voice

cURL
# 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.

3

Create the draft and aggregate quote

cURL · JSON input
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 · multipart
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"}'
Response: 201 Created
JSON
{
  "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.

4

Display the estimate and start the entire batch

Show quote.estimatedTokens, quote.availableTokens, and quote.sufficientBalance before starting.

cURL
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"}'
Response: 202 Accepted

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.

5

Receive webhooks or poll, then retrieve MP3

cURL
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.mp3

Poll 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
JavaScript
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

1

Import media from a URL

cURL
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.

2

Quote and start transcription

HTTP
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-batches

Create a draft narration batch and aggregate quote. Requires Idempotency-Key.

Required scope: projects:write, assets:write, transcriptions:write, renders:write, billing:write, webhooks:read

FieldTypePresenceDescription
titlestringRequiredProject title, 1–200 characters
srtstring/fileRequiredEnglish SubRip content or multipart .srt file
voiceIdstringRequiredID from GET /voices
targetLanguagesstring[]RequiredUnique codes from GET /languages
webhookEndpointIdUUIDRequiredAn active workspace endpoint
externalIdstringOptionalYour 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}/start

Reserve the aggregate quote and queue every language. Requires a new Idempotency-Key.

Required scope: renders:write, billing:write

FieldTypePresenceDescription
quoteIdUUIDRequiredThe 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}/quote

Create a new 15-minute quote for an unstarted, unexpired draft. No request body.

Required 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.

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.

OperationRecommended keyReplay behavior
Create draftcreate-{yourExternalId}Same body returns the existing draft; changed body returns 409
Start batchstart-{yourExternalId}Same quote and key return the existing queued batch
RequoteNot requiredCreates 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

HeaderMeaning
Webhook-IdStable event UUID used for deduplication
Webhook-TimestampUnix timestamp in seconds used for replay protection
Webhook-Signaturev1= followed by the base64url HMAC-SHA256 digest
Content-Typeapplication/json
Node.js signature verification
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 as eventId.timestamp.rawBody, verify it in constant time, reject timestamps older than five minutes, and only then parse the JSON.
narration.job.succeeded

One language completed and its reserved tokens were settled.

JSON webhook body
{
  "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.failed

One language failed and its reserved token share was released.

JSON webhook body
{
  "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.completed

The final aggregate outcome with counts and per-language summaries.

JSON webhook body
{
  "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

GET/webhook-endpoints

List webhook endpoints

webhooks:read
POST/webhook-endpoints

Register HTTPS destination and reveal secret

webhooks:write
PATCH/webhook-endpoints/{endpointId}

Change name, URL, or status

webhooks:write
DELETE/webhook-endpoints/{endpointId}

Soft-delete an endpoint

webhooks:write
POST/webhook-endpoints/{endpointId}/rotate-secret

Rotate and reveal a new secret

webhooks:write
POST/webhook-endpoints/{endpointId}/test

Queue a signed test event

webhooks:write
GET/webhook-endpoints/{endpointId}/events

List delivery history

webhooks:read
POST/webhook-events/{eventId}/replay

Replay with the original event ID

webhooks:write

Errors and safe retries

Every JSON error has a stable code, human-readable message, and request ID. Log the request ID when contacting support.

JSON error envelope
{
  "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 }
  }
}
HTTPCodeMeaning
400invalid_narration_batchMalformed input, invalid SRT, duplicate languages, or missing fields.
400idempotency_key_requiredThe required Idempotency-Key header is missing or invalid.
401unauthorizedThe API key is missing, invalid, expired, revoked, or insufficiently scoped.
402insufficient_tokensThe workspace cannot fund the full batch. No jobs are created.
404webhook_endpoint_not_foundThe endpoint is outside this workspace, missing, or disabled.
404narration_batch_not_foundThe batch is outside this workspace or does not exist.
409idempotency_conflictThe same key was reused with different input.
409invalid_quoteThe quote expired, was finalized, or does not match the batch.
422invalid_srtNo valid cues were parsed, or the transcript exceeds 5,000 cues.
422transcript_too_longNarration text exceeds 40,000 characters.
422invalid_voiceThe 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

POST/narration-batches

Create project, import English SRT, and quote the full batch

multiple write scopes
GET/narration-batches

List workspace narration batches

renders:read
GET/narration-batches/{batchId}

Read quote, jobs, errors, and result URLs

renders:read
POST/narration-batches/{batchId}/quote

Reissue an expired draft quote

renders:write + billing:write
POST/narration-batches/{batchId}/start

Reserve the total and queue every language

renders:write + billing:write

Catalogs

GET/languages?capability=narration

List supported narration languages

renders:read
GET/voices

List selectable voices

renders:read
GET/voices/{voiceId}/preview

Stream a private voice preview

renders:read

Webhook management

GET/webhook-endpoints

List webhook endpoints

webhooks:read
POST/webhook-endpoints

Register HTTPS destination and reveal secret

webhooks:write
PATCH/webhook-endpoints/{endpointId}

Change name, URL, or status

webhooks:write
DELETE/webhook-endpoints/{endpointId}

Soft-delete an endpoint

webhooks:write
POST/webhook-endpoints/{endpointId}/rotate-secret

Rotate and reveal a new secret

webhooks:write
POST/webhook-endpoints/{endpointId}/test

Queue a signed test event

webhooks:write
GET/webhook-endpoints/{endpointId}/events

List delivery history

webhooks:read
POST/webhook-events/{eventId}/replay

Replay with the original event ID

webhooks:write

Advanced project workflow

GET / POST/projects

List or create projects

projects:read / write
GET/projects/{projectId}

Retrieve one project

projects:read
GET / POST/projects/{projectId}/assets

List or reserve private uploads

assets:read / write
POST/projects/{projectId}/assets/imports

Import a public HTTPS audio/video URL

assets:write
GET/projects/{projectId}/assets/imports/{importId}

Read remote import progress

assets:read
GET/projects/{projectId}/assets/{assetId}

Retrieve asset metadata

assets:read
PUT / GET/projects/{projectId}/assets/{assetId}/content

Upload or retrieve asset bytes

assets:write / read
POST/projects/{projectId}/assets/{assetId}/complete

Verify a direct upload

assets:write
GET / POST/projects/{projectId}/assets/{assetId}/multipart

Create or inspect resumable upload

assets:write
PUT/projects/{projectId}/assets/{assetId}/multipart/parts/{partNumber}

Upload one multipart part

assets:write
POST/projects/{projectId}/assets/{assetId}/multipart/complete

Complete multipart upload

assets:write
POST/projects/{projectId}/transcription-jobs

Start quote-backed transcription

transcriptions:write
GET/projects/{projectId}/transcription-jobs/{jobId}

Read transcription progress

transcriptions:read
POST/projects/{projectId}/transcripts

Import parsed transcript segments

transcriptions:write
GET/projects/{projectId}/transcripts/latest

Read latest transcript

transcriptions:read
GET / PATCH/projects/{projectId}/transcripts/{transcriptId}

Read or version transcript edits

transcriptions:read / write
GET / POST/projects/{projectId}/narration-jobs

List or create one narration

renders:read / write
POST/projects/{projectId}/narration-jobs/batch

Create multiple quote-backed narrations

renders:write
GET/projects/{projectId}/narration-jobs/{jobId}

Read narration progress

renders:read
GET/projects/{projectId}/narration-jobs/{jobId}/content

Stream or download MP3

renders:read
POST/billing/quotes

Create a 15-minute action quote

billing:write
GET/billing/wallet

Read available and reserved tokens

billing:read
GET/billing/transactions

Read immutable token ledger

billing:read

Account and token purchasing

GET/billing/packages

List available USD or NGN token packages

public
POST/billing/checkout

Initialize token checkout

billing:write
GET/api-keys

List keys created by the signed-in user

browser session only
POST/api-keys

Create and reveal a new workspace key

browser session only
DELETE/api-keys/{apiKeyId}

Revoke a key created by the signed-in user

browser session only

Media dubbing endpoints — paused for new integrations

POST/projects/{projectId}/render-jobs

Create a source-voice media render

renders:write
GET/projects/{projectId}/render-jobs/{jobId}

Read media-render progress

renders:read
GET/projects/{projectId}/dub-tracks

List completed media dub tracks

renders:read
GET/projects/{projectId}/dub-tracks/{trackId}/content

Stream or download a dub track

renders:read

Need every schema definition?

The machine-readable OpenAPI 3.1 contract is the source of truth for generated clients.

Open OpenAPI YAML