Reference

Endpoints

Every operation of the VocalStack API, each with its TypeScript SDK, Python SDK, PHP SDK and curl sample. Base URL: https://api.vocalstack.com.

batches

  • GET /v1/batches List batches
    The acting principal's batches of files, newest first (by `createdAt`), a page at a time: follow `nextCursor` (absent on the last page) as `cursor`. Each batch carries its counts, not its files (`batches.get`).
  • POST /v1/batches Create a batch
    Start a batch of files: several files transcribed together for less than a transcription each. Add each file or link with `batches.addFile`, then `batches.start`. The batch counts once against the plan's transcriptions created per hour, and is billed by the second of audio, each file at least the plan's minimum per file of a batch (`rates.batchFileMinimumSeconds`, a minute) and the whole batch at least one minimum charge, not one per file (topped up once when the batch is done, if its audio came to less). Its files get the transcript, paragraphs, the audio and the exports, and speaker labels when `settings.speakerLabels` asks; never a summary, a title, a share link or spoken translations. Files per batch is a plan limit (`limits.filesPerBatch`).
  • GET /v1/batches/{id} Get a batch
    A batch with its files (`files`, each a transcription with its job), in the order they were added.
  • DELETE /v1/batches/{id} Delete an unstarted batch
    Discard a batch that was never started, with its files. Nothing was charged. A started batch is stopped instead (`batches.stop`).
  • GET /v1/batches/{id}/export Download a batch's transcripts
    Every finished file's transcript in one `format` (as `recordings.export` makes one), in one zip named for the batch. Files still running or failed are left out; `invalid_state` while none is finished.
  • POST /v1/batches/{id}/files Add a file to a batch
    Add one file or link to an open batch: multipart/form-data with a `file` part (+ optional `options` JSON part), or JSON with `url`, as `transcriptions.create` takes them, with only `name`, `startS`, `endS` and `durationS` among the options (the settings are the batch's). Each file is a transcription of its own (`batchId`), waiting for the batch's start. Refused once the batch has started, or when it already holds the plan's files per batch (`invalid_state`, `data.metric` `files_per_batch`).
  • POST /v1/batches/{id}/start Start a batch
    Seal the batch and queue its files, longest first: the account's free transcriptions at once run that many files side by side, each engine going on to the next file as it finishes one. Needs credit for more than the least the batch can cost (its one minimum charge, or a minute for each file when that comes to more); each file's length is checked against the balance as it starts, as a single file's is.
  • POST /v1/batches/{id}/stop Stop a batch
    Stop every file not finished yet: queued files never start, a running one is cancelled and billed for what it transcribed, with the batch's one minimum.

billing

  • GET /v1/billing Get the account's billing
    The acting account's plan, rates (dollars an hour of audio, and the minimum a transcription is billed: `minimumSeconds`), credit balance, whether running transcriptions go on below zero (`belowZeroLimitOn`; `inBelowZero` while they do; never how far, and `belowZeroLimitChangesAt` when a limit held after a change stops), whether a new transcription would start now and what would refuse it (`canStart`, `startBlock` for a file; `canStartLive`, `startBlockLive` for a live transcription or realtime session, whose minimum costs more), the billing period and the one before, the monthly credit's use, the counted limits in use, the subscription (next payment, a scheduled interval switch, past due), auto top-up with its last attempt, the low-balance alert, a payment still being processed, the default card, a scheduled change with its refund deadline (`pendingChange`), a scheduled end or closure, the days data is kept after a downgrade (`downgradeKeepDays`, with any lowering told) and the auto top-up daily limit (`autoTopUp.autoTopupMaxPerDay`), the retention deadline after a downgrade, the plan's limits and whether it has paid. A creation that would overdraw the balance, or a new transcription when the balance does not cover its minimum, is refused 402 `insufficient_credit`; `topUpUrl` is where to add credit. `enabled: false` means this server does not bill.
  • PUT /v1/billing/auto-top-up Set auto top-up
    Turn auto top-up on (`threshold` and `amount`: any whole-cent values inside the plan list's `autoTopUp.thresholdRange` and `amountRange`; when the balance falls to the threshold, the amount is charged to the saved payment method, at most a few times a day) or off. `409 payment_method_required` until the account has a saved payment method; `409 invalid_state` while the payment provider takes no automatic charges (`autoTopUp.offered: false` on `GET /v1/billing`). The consent (time, hashed address, terms version) is recorded.
  • POST /v1/billing/auto-top-up/retry Run auto top-up now
    Charge the configured auto top-up amount to the saved payment method now, whatever the balance (after a declined attempt, once the card is fixed). Counts against the day's cap: `429 rate_limited` when it is used. `409 auto_top_up_off` when auto top-up is off; `409 invalid_state` while another charge for the account is in progress or automatic charges are not offered. Returns the auto top-up with its `last` attempt (`paid`, `declined` or `pending`).
  • GET /v1/billing/charges List the charges
    The account's per-recording charges, newest first, with the transcription's name: open while the audio runs (metered every minute), settled once the recording ends, void when waived. Page with `cursor`.
  • POST /v1/billing/checkouts Open a checkout
    Start a payment at the payment provider: `{kind: "topup", amount}` adds credit (any whole-cent amount inside the plan list's `topUpRange`), `{kind: "subscribe", planId, interval}` starts a subscription (`month` or `year`); `promoCode` adds a code's bonus credit to a top-up or its discount to a subscription's first payment (`404 promo_code_invalid` when it does not apply; a code counts against the promo check's hourly limit, `429 rate_limited`). Both need `acceptTerms: true` or the request is `422 invalid_body`: the one consent, the Terms and the customer's request that the purchase start now. `waiveWithdrawal` is deprecated and ignored. Send the customer to `url`; the provider returns them to the app's billing page with `?checkout=<id>`. A signed-in person only (for an organization, its owner or admin), never an API key. `409 invalid_state` with an active subscription (cancel it first) or a plan billed by hand. `409 invalid_state` too when the payment provider refuses the call (an address it takes no mail at, a tax ID it rejects), with its reason in `message`: retrying will not help; `503 unavailable` only when it did not answer. Idempotent with `Idempotency-Key`.
  • GET /v1/billing/checkouts/{id} Get a checkout
    A checkout's status, for the page the customer comes back to: by its `checkoutId` or the provider's id (`?checkout=` on the return URL). `succeeded` once the payment landed (the credit or the subscription is then in `GET /v1/billing`).
  • GET /v1/billing/details Get the invoice details
    The name, email, address and tax id invoices and receipts carry. `email` is the sign-in email until another is saved.
  • PUT /v1/billing/details Set the invoice details
    Save the invoice details; they are written onto the provider's customer and prefill every checkout. `country` is ISO 3166-1 alpha-2; an EU VAT ID starts with its country's prefix (Greece: `EL`) and has 8 to 12 letters or digits (spaces, dots and dashes are dropped). `422 invalid_body` names the `field`.
  • GET /v1/billing/ledger List the ledger
    The account's credit movements, newest first: signup credit, top-ups, monthly credit, charges, corrections, refunds. Page with `cursor`.
  • PUT /v1/billing/low-balance-alert Set the low-balance alert
    Turn the low-balance email on or off and set its line (`threshold`, inside the plan list's `lowBalanceAlert.range`): one email each time the balance falls to it.
  • GET /v1/billing/payment-methods List the payment methods
    The cards saved at the payment provider: brand, last 4, expiry, wallet, which is the default, and whether the last automatic charge on it was declined. Empty before the first payment or card. For an organization, its owner or admin.
  • POST /v1/billing/payment-methods/manage Add or change a payment method
    A link to the payment provider's portal, where a card is added, replaced or made the default (the provider has no API for those); it returns to the app's payment tab. Works before the first payment.
  • DELETE /v1/billing/payment-methods/{id} Remove a payment method
    Remove a saved card. `409 payment_method_in_use` while it pays for the subscription. Removing the last one turns auto top-up off.
  • GET /v1/billing/payments List the payments
    The account's payments history, newest first: top-ups, auto top-ups and subscription payments (`paid`, `pending` while the provider processes one, `failed` for a declined charge or renewal), refunds (`refunded`) and credit never paid for (`credit`: the signup credit, a promo code's bonus, an adjustment). `document` says whether an `invoice` or a `receipt` can be fetched. `businessPurchase` and `taxId`: a purchase made with a tax ID (on the invoice details or given at checkout) is a business purchase. Page with `cursor`.
  • GET /v1/billing/payments/{id}/document Get a payment's invoice or receipt
    A link to the payment's invoice (a subscription) or receipt (a top-up), valid for a short while. The provider makes it on the first ask: `202` with `{pending: true}` until then, ask again in a moment. `409 billing_details_required` when it needs a billing name and address (`PUT /v1/billing/details`). A signed-in person only (for an organization, its owner or admin), never an API key: the first ask writes the document with the invoice details as they stand.
  • GET /v1/billing/plans List the plans
    The visible plans with their current rates (with the minimum a transcription is billed), prices, included credit, whether running work goes on below zero when the credit runs out (`belowZeroLimitOn`, never how far), limits and features, plus the top-up presets and range, the auto top-up and low-balance alert choices and ranges, the auto top-up daily limit, the signup credit and the days data is kept after a downgrade (`downgradeKeepDays`). These are the figures for new customers; an account's own are on `GET /v1/billing`. No credential needed.
  • GET /v1/billing/portal Get the customer portal link
    A link to the payment provider's customer portal (payment methods, invoices, receipts), valid for a short while. `409 payment_method_required` before the first checkout.
  • POST /v1/billing/promo-codes/check Check a promo code
    What a code gives `for` a `topup` (a bonus credit) or a `subscribe` (a discount off the first payment), before the checkout. `404 promo_code_invalid` when it does not exist, has expired, is used up, was redeemed by this account or gives nothing for this purchase. Rate-limited per account (a checkout carrying a `promoCode` counts against the same limit). For an organization, its owner or admin.
  • POST /v1/billing/subscription/cancel Cancel the subscription
    Cancel at the end of the current period: nothing changes until then; after it the account moves to the default plan and unspent monthly credit is removed. `409 invalid_state` without a subscription.
  • POST /v1/billing/subscription/interval Switch the billing interval
    From the next period the subscription renews `month`ly or `year`ly (nothing is charged now) at `subscription.switchPrices` for that interval, the price of the version its next renewal is on; that price is also kept as a ceiling on what it renews on until a price change announced with its own date applies. Needs `acceptTerms: true` and `noNewWithdrawalPeriodShown: true` (the customer was shown that the switch starts no new 14-day withdrawal period) or the request is `422 invalid_body`; the consent is recorded. Send `expectedPrice` (the price shown) to get `409 invalid_state` when it changed meanwhile. `subscription.pendingInterval` shows the switch until it applies, with its date once the payment provider holds it. `409 invalid_state` without a subscription, when it ends at the period end, for an interval the plan does not offer, or a switch to `month` while monthly billing is withdrawn (`subscription.yearlyOptIn`); `503 unavailable` when the payment provider did not take or confirm it (nothing changed).
  • DELETE /v1/billing/subscription/interval Keep the billing interval
    Take back a scheduled interval switch.
  • POST /v1/billing/subscription/uncancel Keep the subscription
    Take back a cancellation scheduled for the period end. `409 invalid_state` while monthly billing is no longer offered on the plan (`subscription.yearlyOptIn`): the yearly opt-in keeps the subscription then.
  • POST /v1/billing/subscription/yearly-opt-in Switch to yearly to keep the subscription
    When a plan change withdraws monthly billing, a monthly subscription is never moved onto a yearly charge by itself: it ends at the renewal the change applies at (`subscription.yearlyOptIn: true`, `cancelReason: "interval_withdrawn"`) unless the customer opts in to yearly here, even after that cancellation is set. The subscription is kept, and renews yearly at `subscription.switchPrices.year` from that renewal (nothing is charged now). Needs `acceptTerms: true` and `noNewWithdrawalPeriodShown: true` (`422 invalid_body` otherwise); `expectedPrice` as for the interval switch. `503 unavailable` when the payment provider did not confirm it: nothing changed and the subscription still ends. Without a withdrawal it is an ordinary switch to yearly.
  • GET /v1/billing/usage Get the usage report
    What the account spent per day (in `tz`), per source (`dashboard` or the API key that made the transcription, deleted recordings included) and per kind (`file`, `live`): audio seconds and dollars, with running transcriptions at their current amount. `startDate` and `endDate` are inclusive days (`YYYY-MM-DD`); a start before the account's first day is moved to it and an end after today to today (the answer's `startDate` and `endDate` say what was used); at most 92 days (`400 range_too_long`).
  • GET /v1/billing/usage/charges List the usage report's charges
    The charges behind the usage report, one by one, newest first: each file or link transcription, live recording and realtime session billed in the range (a running one at its current amount, `state: open`), with the transcription's name, its kind, its source (`dashboard` or the API key's id, as the report's `sources`), the seconds billed, the rate and the cost; money given back for a charge is a `correction` with a negative amount. `count` and `total` cover every charge matching the filters, not just the page. The range is the usage report's (`startDate`, `endDate`, `tz`: inclusive days, at most 92, `400 range_too_long`); `kind` (`file` or `live`) and `source` narrow it. Page with `cursor`.

broadcasts

  • GET /v1/broadcasts List broadcasts
    Broadcasts owned by the acting principal. `lifecycle=all` is the active and the archived in one answer (each says which it is in its `state`), up to 500 of each — what a dashboard that shows them all should ask, once.
  • POST /v1/broadcasts Create a broadcast
    Create a broadcast: the link listeners open (`code`), which a host can change later (the old one keeps working).
  • POST /v1/broadcasts/archive Archive broadcasts
    Archive several broadcasts in one call (up to 500 `ids`), each as the one-broadcast call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/broadcasts/purge Purge broadcasts
    Purge several broadcasts in one call (up to 500 `ids`), each as the one-broadcast call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/broadcasts/restore Restore broadcasts
    Restore several broadcasts in one call (up to 500 `ids`), each as the one-broadcast call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • GET /v1/broadcasts/{id} Get a broadcast
    One broadcast of the acting principal's, by id.
  • PATCH /v1/broadcasts/{id} Update a broadcast
    Rename, change access/password, change the link (`code`; the links it had keep opening it), or update default capture settings.
  • POST /v1/broadcasts/{id}/archive Archive a broadcast
    Archive the broadcast: hidden, its links dead, its data kept until it is restored or purged.
  • POST /v1/broadcasts/{id}/audio-sources Add an audio source
    Give a broadcast a stream it can record from — a YouTube live, an internet radio address (`audioSources`). `broadcasts.startRecording` with `audioSource` set to its id records it: each such recording is a live transcription of its own, attached to the broadcast. Returns the broadcast.
  • DELETE /v1/broadcasts/{id}/audio-sources/{source} Remove an audio source
    Take an audio source off a broadcast. What was recorded from it stays. Returns the broadcast.
  • GET /v1/broadcasts/{id}/audio-sources/{source}/check Check an audio source
    Whether an audio source can be recorded right now: the server opens it as a recording would — a YouTube link resolved to its live stream, a radio address connected, a playlist followed, the first audio decoded — and says whether audio came back (`state`). When it did not: what went wrong (`problem`, `message`, `httpStatus`), and, when one of the likely mistakes — an extension a letter off, the other scheme, the stream a web page plays — was opened and gave audio, that address (`fix`). Nothing is recorded. A ready answer stands for a minute and a failed one for 20 s, unless `fresh` asks again.
  • POST /v1/broadcasts/{id}/purge Purge a broadcast
    Delete an archived broadcast and everything in it, for good.
  • GET /v1/broadcasts/{id}/recordings List a broadcast's recordings
    The broadcast's recordings, newest first, a page at a time: follow `nextCursor` (absent on the last page) as `cursor`. `lifecycle=archived` lists the archived ones.
  • POST /v1/broadcasts/{id}/recordings Start a recording
    Start recording on the broadcast. The recording is made in a new live transcription that hears the broadcast's microphone — or, with `audioSource`, one of its audio sources — and is attached to it: `recording.transcriptionId` names that transcription, the broadcast's link shows it (`currentRecording`), and it is listed with the transcriptions. Mints a publisher realtime session (`streamUrl`, `sessionId`); 409 conflict with details.takeover when another device is recording. `scriptText` gives the recording words to follow like a teleprompter (`Recording.scriptText`).
  • POST /v1/broadcasts/{id}/restore Restore a broadcast
    Bring an archived broadcast back.
  • GET /v1/broadcasts/{id}/share-links List a broadcast's share links
    Active share links of the broadcast.
  • POST /v1/broadcasts/{id}/share-links Create a share link
    A named link ("Who is it for?") to the read-only public page. `allowDownload`, `allowExport` and `allowTranslate` are all on by default and per link. Send `allowTranslate: false` to close a link to translation; it takes effect from the next request.

connect

  • POST /v1/connect/token Redeem a connect code
    One-click Connect ([guide](/docs/guides/connect-an-app.md)): your server swaps the `code` the dashboard sent to your `redirect_uri` for an API key, with the PKCE `code_verifier` (OAuth 2.0 authorization code + PKCE; the access token is the key). No credential. The code works once and for two minutes; a second redemption, a wrong verifier or another `redirect_uri` is `400 invalid_grant`, and the code is spent either way. A request from a web page is refused: call it from your server. Errors are OAuth's `{ error, error_description }`, never the API's error envelope, but for `429` and `503`. The answer is `Cache-Control: no-store`.

corrections

  • POST /v1/corrections/{id}/revert Revert a correction
    Undo one correction.
  • GET /v1/recordings/{id}/corrections List corrections
    The corrections made to the recording's transcript, newest first, a page at a time: follow `nextCursor` (absent on the last page) as `cursor`.
  • POST /v1/recordings/{id}/corrections Apply corrections
    Apply a batch of word edits and line rewrites atomically; every target must be settled. Each correction says whether its line is voiced as corrected (`voiced`): the voice may only say what was said, so a correction is voiced only when every word it says is a word of the recognizer's text (punctuation, case, Latin accents and number formatting aside) or of a fresh reading of the line's audio, taken automatically when the recognizer's text is not enough, and it drops no negation the audio has. A line spanning several segments lists neighbouring segments only (400 `invalid_options` otherwise). Any other correction is saved and read, but voice clips and voice tracks keep speaking what the recognizer heard (`notVoicedReason: rewrite`). VocalStack speaks transcripts, never text a caller wrote.
  • POST /v1/recordings/{id}/retranscribe Retranscribe a line
    Read one line's audio again with the language forced, and apply what the recognizer hears as a line rewrite. A line heard in the wrong language cannot be repaired word by word — every word is wrong together — so this is the repair for it. The segments must be settled, like every other correction; the answer is the same `line` corrections a hand-typed rewrite produces, with the same revert behind them. 503 `engine_unavailable` means the audio for that line has not reached this machine yet and the ask is worth repeating; 503 `unavailable`, that the line could not be read just now. `language_unavailable`: lines of this transcription cannot be read again in another language (its engine has no second model).

jobs

  • GET /v1/recordings/{id}/job Get a job
    The recording's transcription job: its state and progress (done/total segments, media seconds, pct). A `queued` job with `queue` is waiting its turn behind the account's other transcriptions (the plan's `concurrentTranscriptions` are running) and starts by itself when one ends; `queue.position` 1 is the next to start.
  • GET /v1/recordings/{id}/job/preview Get a job's preview media
    A job waiting in `cropping`: the media for the trim step to draw the waveform from in the browser. From a link (`preview: true`), the file the server fetched — read once, kept until the job is started or for ten minutes, never stored; from an upload, its stored source streamed through. 404 once a link's copy is gone.
  • POST /v1/recordings/{id}/job/retry Retry a job
    Re-queue a failed job from its stored source. Past the plan's transcriptions at once it waits its turn (`queue`) behind the account's other queued jobs instead of being refused.
  • POST /v1/recordings/{id}/job/start Start a cropped job
    Release a job waiting in `cropping` (created with `crop: true`): the window of the source to transcribe, in seconds — an omitted bound is the start or the end of the file — then the job is queued. Timestamps in the transcript count from the window's start. Past the plan's transcriptions at once it waits its turn (`queue`) instead of being refused.

languages

  • GET /v1/languages List languages
    Transcription, translation and voice lists with picker metadata.

me

  • GET /v1/me Get the current account
    Who the credential is: the acting principal, the person (`userId`) or API key (`apiKeyId`) behind it, the principals it may act for, a key's scopes and its reach (`own`: only what the key created; `account`: everything on the account).

meta

  • GET /v1/clients/{client} Get an app's version policy
    For VocalStack's own apps (`mac_app`: VocalStack for Mac): the newest build, with its download, size, SHA-256 and the release key's Ed25519 signature, and below which versions the app is deprecated or unsupported. `latest` is `null` while there is no build to offer. No credential.
  • GET /v1/health Check health
    Liveness: 200 while this server is up.
  • GET /v1/openapi.json Get the OpenAPI document
    This API's OpenAPI document, with this host in `servers`.

realtime

  • GET /v1/realtime/capabilities Get realtime capabilities
    What the realtime socket takes: supported languages, audio formats, and limits.
  • POST /v1/realtime/sessions Create a realtime session
    Create a realtime session: a signed, stateless token for one WebSocket (`streamUrl`). Nothing is stored server-side.
  • POST /v1/realtime/warm Keep an engine ready
    Say that a person is using a screen that can start a live transcription (`canStart`), so the live transcription they start next is ready at once instead of waiting for an engine to load. Send it on real use — input, focus, a visible window — at most every 30 seconds, and at once when the spoken language selected there changes; an open window nobody uses should not send it. Two minutes after the last one, nothing is kept. `spokenLanguage` is the language selected on that screen (absent for auto-detect). It starts nothing and bills nothing.

recordings

  • POST /v1/recordings/archive Archive recordings
    Archive several recordings in one call (up to 500 `ids`), each as the one-recording call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/recordings/purge Purge recordings
    Purge several recordings in one call (up to 500 `ids`), each as the one-recording call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/recordings/restore Restore recordings
    Restore several recordings in one call (up to 500 `ids`), each as the one-recording call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • GET /v1/recordings/{id} Get a recording
    One recording, by id: for its owner, or any listener who can reach its broadcast or transcription.
  • POST /v1/recordings/{id}/archive Archive a recording
    Archive the recording: hidden, its data kept.
  • GET /v1/recordings/{id}/audio Download a recording's audio
    302 to a freshly signed URL. kind=delivery (owner + listeners, 1 h), voice (a rendered spoken track, needs lang=, owner + listeners, 1 h; through a share link a language other than the spoken one needs the link's `allowTranslate`; a track not made yet is queued, from the clips already spoken where there are any, and answers 503 engine_unavailable with `retryAfterS` until it is ready, or 429 rate_limited when the recording already has as many readings being made as it may), background (the recording's sound with the voices taken out, music and ambience, to play under a spoken track; owner + listeners, 1 h; format=ogg or caf), speech (the recording's sound with the background taken out: the speech alone, without music, ambience or noise, aligned with the original to play in its place; same access; format=ogg or caf). Both are made for every recording while it is transcribed, piece by piece (a live recording once it ends; earlier recordings are queued once); asking for them never starts the work. Until the whole track is ready they answer 503 engine_unavailable with `retryAfterS`, `readyUntilS` (the media seconds already separated, from 0) and `parts`: `[{startS, endS, url}]`, consecutive pieces covering 0 to `readyUntilS`, each a signed URL (1 h) in the asked format, to play in place of the original up to `readyUntilS`. The other kinds: source (owner or a link that allows downloads), video (the source's video as it came, kept beside its audio on a plan that stores source video, Job.hasVideo; owner or a link that allows downloads) or master (owner, 5 min). Through a share link EVERY kind needs the link's `allowDownload` — streaming a file and saving it are the same request, so a link with audio off plays nothing either. The delivery is Opus in two containers: format=ogg (default) and format=caf, the one Safari/iOS plays (WebKit rejects or mis-times Ogg); pick by canPlayType('audio/x-caf; codecs=opus'). format=mp3 is for SAVING (delivery and voice): the one container every player opens, encoded on first request and kept (kinds delivery_mp3, voice_mp3:<lang>). The recording's `media` lists which renditions are sealed (kinds delivery, delivery_caf, delivery_mp3); the CAF is remuxed on first request when it is not there yet.
  • GET /v1/recordings/{id}/audio/clip Get an audio clip
    A stretch of the audio as WAV (16-bit, 16 kHz, mono), available while the recording is still open — for replaying a line during live correction. Owner + listeners; at most 120 s. Through a share link it needs the link's `allowDownload`, like every other way of asking for the audio.
  • GET /v1/recordings/{id}/events Get a recording's event log
    The recording's durable event log in order, each frame stamped with the wall time it was written — replays the recording exactly as the host saw it. A page at a time: follow `nextCursor` (absent on the last page) as `cursor`, or start after a `seq` you hold with `after` (not both). Owner only.
  • GET /v1/recordings/{id}/export Export a recording
    A real file: JSON, SRT, XLSX (Excel workbook) or PDF, corrections included. The PDF of a finished recording reads in paragraphs (built by the engine after finalize; a download that finds them missing or stale has them rebuilt first, waiting up to a minute). Owner, a share link (`shareCode`) that allows export, or a listener of its broadcast (`listenerToken`) once the recording has ended. While the job runs the export is partial (file name suffixed, `partial` flagged). `lang` makes the file read in that language — the translation the reader sees on screen, one line per sentence, sentences not yet translated are translated first; the file name carries the language and JSON's `translated` is true. The spoken language, or no `lang`, is the transcript as spoken. A share link with Translate off may only have that.
  • GET /v1/recordings/{id}/media-token Get a media token
    The owner's credential for this recording's media URLs: pass it as `media=` to `/audio`, `/audio/clip` and `/voice/{sentence}` in place of `access_token`. It reads this recording's media and nothing else, and it is the same string for a whole UTC day (valid until the end of the next one) — an access token rotates every few minutes, and a credential in the URL is part of the browser's cache key, so clips fetched with one were fetched again at every rotation.
  • POST /v1/recordings/{id}/pause Pause a recording
    Pause capture. Idempotent.
  • POST /v1/recordings/{id}/publish-token Create a publish token
    Re-attach a publisher (reconnect or another device; takeover=true demotes the current one).
  • POST /v1/recordings/{id}/purge Purge a recording
    Delete an archived recording for good.
  • POST /v1/recordings/{id}/restore Restore a recording
    Bring an archived recording back.
  • POST /v1/recordings/{id}/resume Resume a recording
    Resume a paused recording.
  • PATCH /v1/recordings/{id}/settings Update recording settings
    Mid-recording capture settings (mode, language, labels).
  • PUT /v1/recordings/{id}/speakers/{idx} Name a speaker
    Name a speaker of this recording.
  • POST /v1/recordings/{id}/stop Stop a recording
    End capture; finalize runs asynchronously. A live recording stops only from paused. A file transcription's recording stops at any point before its job is done: the job fails with `errorCode` `stopped`, and what was transcribed before the stop is kept (and billed; nothing is billed when it had not started transcribing).
  • POST /v1/recordings/{id}/summarize Request a summary
    Ask for the recording's AI title and summary. A recording made with `summary` gets them after finalize on its own; asking turns `summary` and `title` on for one made without them. Call this when a finished recording has none (or its pass failed) and poll the recording: `ready` carries them, `pending` means the pass is queued or running, `not_requested` that the recording is under a minute or a file of a batch (never summarised), `unavailable` that this server has no summariser or the recording has no transcript (nothing was said).
  • GET /v1/recordings/{id}/summary Get a recording's summary
    The title and summary as they stand — `ready` once there is a summary (the title is the recording's name when it was made without `title`), `pending` while the engine is still writing them, `not_requested` for a recording made without `summary`, shorter than a minute, or a file of a batch, `unavailable` on a server without a summariser. Nothing is queued (that is `summarize`), so a share-link viewer may ask too. `lang` reads them in that language, for a recording made with `summaryTranslations`: the one the reader picked on screen, which the transcript and the downloads follow. The engine translates the summary into every language the recording was listened to or read in as it writes it, so those are ready; any other language is translated once, on the first ask, and kept. `lang` in the answer says the translation was had. The spoken language, or no `lang`, is the summary as written. Through a share link a language other than the spoken one needs the link's `allowTranslate`.
  • GET /v1/recordings/{id}/transcript Get a transcript
    Snapshot up to lastSeq; the backfill for subscribers (after=cursor). A line scoring under `settings.confidence.hideBelow` (0.60 by default — the line's own mean word probability, not a count of bad words) is hidden as "Audio unclear". Listeners see it blanked (`suppressed`, text=''); anyone shown the text — the owner, a share-link reader — gets `unclear` instead and keeps every word. Either way the line is spoken to nobody: `GET .../voice/{sentence}` answers 404 for it. Listeners also see translations filtered to lang. Through a share link (`shareCode`), `lang` needs the link's `allowTranslate`: without it any language other than the one spoken is `forbidden`, never a silently untranslated snapshot.
  • GET /v1/recordings/{id}/translation Get a translation
    Sentences of the transcript translated into `lang` (a translation target), by sentence id (`sentences`, comma separated): what a reader in another language pages through. The owner, or any listener who can reach its broadcast or transcription; through a share link it needs `allowTranslate`. A listener's new (recording, language) pairs are limited per network an hour.
  • GET /v1/recordings/{id}/voice Get a recording's voice tracks
    What this recording can be heard speaking, one row per language: rendered tracks (`sealed`) and their jobs, and the per-sentence clips a live audience left behind (`clips`, with the sentence ids and the voice that spoke them). An ended recording has only clips — this is how a listener finds them again and plays its spoken translation back a line at a time. The owner, or any listener who can reach its broadcast or transcription; `lang` narrows it to one. A spoken translation needs the link's `allowTranslate` when reading through a share link — without it the listing carries only the spoken language, and asking for another is `forbidden`.
  • POST /v1/recordings/{id}/voice Render voice tracks
    Render spoken tracks for a finished recording, one job per language (1–10 codes, each one /v1/languages says has a voice). The owner, or any listener who can reach its broadcast or transcription (`listenerToken`, `shareCode`, or a bearer token in `access_token`); through a share link a language other than the spoken one needs the link's `allowTranslate`. `voice` reads every speaker in one voice and `voiceBySpeaker` gives each speaker their own; with neither, the recording's default cast speaks (each speaker's granted clone when `voiceCloning` is on, else distinct stock voices of their gender). Every voice named must be one `GET /v1/voices/available?lang=…&recording={id}` offers for each language.
  • GET /v1/recordings/{id}/voice/{sentence} Get a spoken sentence
    One spoken sentence, Ogg/Opus (`format=caf` for Apple's media stack). `sentence` is a sentence's `sentenceId` from the transcript (`sentences[].sentenceId`), not a segment's position; one the recording does not have is a 404. With no `voice`, each speaker is read in their own cloned voice when its consent is granted and the recording has `voiceCloning` on, else in a stock voice of their gender, a different one for each speaker. The `X-Voice-Key` response header names the voice, or the per-speaker cast, the clip was read in. The owner, or any listener who can reach its broadcast or transcription. A clip in a language the recording was not spoken in is a spoken translation: through a share link it needs `allowTranslate`.

rtmp

  • GET /v1/rtmp Get RTMP status
    The account's RTMP server URL and stream key for an encoder, whether one is sending audio right now and what (`audio`, e.g. "48 kHz AAC"), and a signed link to listen to it (`monitorUrl`, an hour). `live` and `audio` cover every key of the account: this key and its app keys (`GET /v1/rtmp/keys` says which one is sending). The server URL is RTMPS (TLS) where the server has it; the plain `rtmp://` form on the same host works for encoders without TLS. A live transcription takes it as its source with `source.url` = `serverUrl`. The key is made on first ask, and that answer is the only one with the key in full (`streamKey`): it is kept as a hash, and later answers show `maskedStreamKey`. A lost key is replaced by a reset in the VocalStack app, which revokes the audio links issued before it. An app that streams (a plugin in the encoder, say) makes a key of its own with `POST /v1/rtmp/keys` rather than using this one.
  • GET /v1/rtmp/keys List app stream keys
    The account's app stream keys, oldest first: masked (`maskedStreamKey`), never in full, each with whether an encoder is sending on it now (`live`, `audio`). A key whose API key no longer works (revoked, expired or suspended, with no replacement from a rotation) is not listed: it no longer publishes. An API key whose reach is `own` lists the keys it made.
  • POST /v1/rtmp/keys Create an app stream key
    A stream key of the app's own, so it can stream to the account without the account's key (`GET /v1/rtmp`), which an encoder may already be using. Audio sent on it reaches the account's RTMP source exactly as on the account's key: the same `serverUrl`, the same live transcriptions, the same monitor; the newest publisher on any of the account's keys is the one heard. The answer is the only one with the key in full (`streamKey`); it is kept as a hash. Made with an API key, it works while that key does (or its replacement, after a rotation): revoking that API key (in the VocalStack app: Developers, API keys) stops whatever streams on it within seconds. At most 20 working keys per account (`409 conflict` past that).
  • DELETE /v1/rtmp/keys/{id} Delete an app stream key
    The key stops working: an encoder publishing on it is disconnected within seconds. The account's key and its other app keys are not affected.

search

  • GET /v1/search Search transcripts
    Sentence search via the English pivot ∪ literal match; results rendered into lang on demand. An owner searches their own transcriptions and broadcasts; a listener names the broadcasts to search by their codes.

share-links

  • PATCH /v1/share-links/{id} Update a share link
    The share sheet's toggles: any of `label`, `allowDownload`, `allowExport`, `allowTranslate`. Omitted fields are left as they are.
  • DELETE /v1/share-links/{id} Revoke a share link
    Revoke: the public page 404s at once.
  • GET /v1/shares Find a published share of a link
    With no credential: the share code of the newest finished transcription of a video or audio link that a VocalStack player published on a site (played there, with a share link). Only transcriptions published through the player on that same `site` are found, never other share links. Open the code with `GET /v1/shares/{code}`. `code` is `null` when the link has not been published there.
  • GET /v1/shares/{code} Open a share code
    What a share link's code opens, with no credential: the transcription's name, who shared it (`sharedBy`, a display name only), what the link allows (`allowDownload`, `allowExport`, `allowTranslate`), its latest recording (`recording`, read as a listener sees it) and, for a file or a link, the recording's job. Pass the same code as `shareCode` to the recording's reads: `GET /v1/recordings/{id}/transcript`, `/summary`, `/translation`, `/voice`, `/voice/{sentence}`, `/audio`, `/audio/clip`, `/export` and the `subscribe` socket. A revoked or unknown code is `not_found`, and so is a link whose transcription has no recording yet. Each call counts as a view of the link.

tags

  • GET /v1/tags List tags
    The account's tags, by name. Each transcription lists its own in `tags`.
  • POST /v1/tags Create a tag
    A tag: `name` (1 to 40 characters, unique in the account however it is cased: `conflict` when taken) and `color`, one of `coral`, `amber`, `lime`, `green`, `teal`, `blue`, `violet`, `pink` — when omitted, the first no tag of the account has yet. An account keeps up to 200 tags.
  • PATCH /v1/tags/{id} Update a tag
    Rename or recolour a tag: any of `name`, `color`. Omitted fields are left as they are. Every transcription with it shows the change, so a key that reaches only its own transcriptions is `forbidden`.
  • DELETE /v1/tags/{id} Delete a tag
    Delete a tag: it comes off every transcription it is on. The transcriptions stay. A key that reaches only its own transcriptions is `forbidden`.
  • POST /v1/tags/{id}/transcriptions Tag transcriptions
    Put the tag on the transcriptions in `add` and take it off those in `remove`, by id, up to 500 in one call. All or none: an id that is not one of yours is `not_found` and nothing changes. Adding a tag a transcription already has, or removing one it does not, is not an error. Returns the tag.

transcriptions

  • GET /v1/transcriptions List transcriptions
    Transcriptions owned by the acting principal (file, link and live), newest first (by `createdAt`), a page at a time: follow `nextCursor` (absent on the last page) as `cursor`. A cursor is never an offset: transcriptions created, changed or archived while you page are neither repeated nor skipped among the rest. `lifecycle=all` is the active and the archived in one list. The filters narrow the list as the dashboard's search and filters do, and every one given must match; `total` counts the matches across every page. `shared=true` keeps the transcriptions with a live share link, and each one's `shareCode` plays in `@vocalstack/player` with no key.
  • POST /v1/transcriptions Create a transcription
    Transcribe a file or a link: creates the transcription, its recording and a server-owned job. multipart/form-data with a `file` part (+ optional `options` JSON part), or JSON with `url` (fetched server-side). The source is kept in the media store. Follow the job with `jobs.get`. Never refused for the plan's transcriptions at once: past that limit the transcription is created all the same and its job waits `queued`, with `job.queue`, until one of the account's own transcriptions ends, then starts by itself (the account's jobs in the order they were queued); its charge opens when it starts.
  • POST /v1/transcriptions/archive Archive transcriptions
    Archive several transcriptions in one call (up to 500 `ids`), each as the one-transcription call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/transcriptions/live Create a live transcription
    Create a live transcription: captured as it is spoken — this device's microphone, or a stream the server pulls — rather than read from a file (`live: true`). It is created idle; `transcriptions.startRecording` starts it, and the recording's pause / resume / stop calls drive it. `broadcasts` attaches it to those broadcasts from the start. A stream `source.url` may be a web page that plays one (a radio station's page): the first live stream found on it becomes `source.url`, and a page with none is refused (`invalid_options`); searching a page counts against the link check's hourly allowance of web pages.
  • GET /v1/transcriptions/lookup Find a link's transcription
    The caller's own transcription of a link, if there is one: the newest active (not archived or purged) transcription whose recording was transcribed from `url`. A YouTube link matches any link to the same video (`watch?v=`, `youtu.be`, `shorts`, `embed`, `live`); any other link matches the same URL, its scheme and host in any case, without its `#fragment` and with or without a trailing slash. A finished one is preferred to one still being transcribed, and either to one that failed or was stopped. `transcription` is null when there is none; the link is not fetched.
  • GET /v1/transcriptions/probe Probe a link
    What a link is before it is transcribed: `live` — a YouTube live on air or scheduled, an endless HTTP stream, an HLS playlist with no end, a dynamic DASH manifest — belongs in a live transcription; `file` is transcribed as one; `unknown` could not be told. `source` names the live form's source (`youtube`, `radio`). A web page is not a recording (`ok: false`), but `media` lists the recordings and live streams on it — media files, playlists, YouTube videos, a station's stream — each found, then opened as a link of its own would be, with its title on the page when it has one and its `kind` (`live` or `file`): create one transcription per `file` entry. A page whose own player plays a live stream (a radio station's page) is `kind: live` and `ok: true`, its stream first in `media`: a live transcription made from the page records that stream. When `ok` is `false`, `reason` says why for a client to choose its own next step (`web_page`, `site_not_fetched` and `meeting` with `siteName`, `not_found`, `unreachable`, `forbidden`, `not_media`, `copy_protected`, `too_long`, `unknown`) and `problem` says what is wrong in a sentence for people, never what to do: a link that is plainly not a recording (a PDF, an image) or is copy-protected is refused, and so is a YouTube or Loom video longer than a file may be (8 hours). A Loom share link is a `file` when the video is public; a private one is `ok: false` with `forbidden`. Nothing is kept. 60 a minute; searching a web page also counts against an hourly allowance — 20 with a plan, 10 on pay as you go, 2 on credit never paid for — and past it the probe answers `rate_limited` with `retryAfterS`.
  • POST /v1/transcriptions/purge Purge transcriptions
    Purge several transcriptions in one call (up to 500 `ids`), each as the one-transcription call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/transcriptions/restore Restore transcriptions
    Restore several transcriptions in one call (up to 500 `ids`), each as the one-transcription call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • GET /v1/transcriptions/{id} Get a transcription
    One transcription of the acting principal's, by id.
  • PATCH /v1/transcriptions/{id} Update a transcription
    Rename, or update default capture settings.
  • POST /v1/transcriptions/{id}/archive Archive a transcription
    Archive the transcription: hidden, its links dead, its data kept until it is restored or purged.
  • PUT /v1/transcriptions/{id}/broadcasts/{broadcast} Attach a broadcast
    Feed a broadcast from the live transcription `id`: listeners on the broadcast's link read the transcription while it records, and its recordings are listed with the broadcast's own. One transcription can feed several broadcasts, and a broadcast can be fed by several. Returns the transcription.
  • DELETE /v1/transcriptions/{id}/broadcasts/{broadcast} Detach a broadcast
    Stop feeding the broadcast from the live transcription `id`. Returns the transcription.
  • POST /v1/transcriptions/{id}/purge Purge a transcription
    Delete an archived transcription and everything in it, for good.
  • GET /v1/transcriptions/{id}/recordings List a transcription's recordings
    The transcription's recordings, newest first, a page at a time: follow `nextCursor` (absent on the last page) as `cursor`. `lifecycle=archived` lists the archived ones.
  • POST /v1/transcriptions/{id}/recordings Start a recording
    Start recording a live transcription and mint a publisher realtime session (`streamUrl`, `sessionId`). 409 conflict with details.takeover when another device is recording. When its source is a stream URL the server pulls the URL and publishes it itself: the grant carries no `streamUrl`, and `recording.ingest` (plus `stream.ingest` frames) says where it stands. `scriptText` gives the recording words to follow like a teleprompter (`Recording.scriptText`).
  • POST /v1/transcriptions/{id}/recordings/upload Upload a recording
    A file or link recording in this transcription: no live phase, transcribed at full speed.
  • POST /v1/transcriptions/{id}/restore Restore a transcription
    Bring an archived transcription back.
  • GET /v1/transcriptions/{id}/share-links List a transcription's share links
    Active share links of the transcription.
  • POST /v1/transcriptions/{id}/share-links Create a share link
    A named link ("Who is it for?") to the read-only public page. `allowDownload`, `allowExport` and `allowTranslate` are all on by default and per link. Send `allowTranslate: false` to close a link to translation; it takes effect from the next request.

voices

  • GET /v1/voices List voices
    The account's cloned voices, newest first. `lifecycle=archived` lists the archived ones instead.
  • POST /v1/voices Create a voice
    Create a voice from a recorded sample and queue its training: multipart/form-data with a `sample` audio part and a `meta` JSON part. The sample needs at least 20 seconds of speech from one person: it is diarized, and the speaker with the most speech is cloned; the silence around them does not matter. The voice comes back `training` with progress 0; poll GET /v1/voices/{id}. Training reports a real state — it never fakes progress, and a voice that cannot be trained ends `failed` with the reason on `error`.
  • POST /v1/voices/archive Archive voices
    Archive several voices in one call (up to 500 `ids`), each as the one-voice call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • GET /v1/voices/available List available voices
    The voices that can speak a language, best first (`default` marks the one a recording that pins none gets). Open to listeners; a bearer token also returns that account's saved cloned voices (not automatic clones). Cloned entries carry `consent`, `auto` and, with `recording`, `speaker`. A clip is synthesised once per language and fanned out, so this is the set a host may pin in `settings.voiceByLang` — and all a listener can ever be offered.
  • POST /v1/voices/delete Delete voices
    Delete several voices in one call (up to 500 `ids`), each as the one-voice call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • POST /v1/voices/from-speaker Create a voice from a speaker
    Create a voice from one speaker of a recording — no sample to upload, because the recording's own audio is the sample. Works while the recording is still running: the server joins the stretches that speaker actually spoke for and clones from those. Naming them here also labels them in the transcript and associates them with the new voice. Needs at least 20 seconds of that speaker's speech, their diarized turns added up; the refusal says how much there is. `consent: true` grants consent; when the speaker was already cloned automatically (and not refused), that clone is the one granted, named and saved (`auto` becomes false: it is kept when the recording goes). `spans` picks the stretches to clone from (media seconds, e.g. the transcript's sentences of that speaker): only that speaker's speech inside them is used, they must add up to 5–60 seconds (about 25 is plenty; under 20 makes a weaker voice: the clearest complete sentence among them becomes the voice's reference), and a new voice is always made. `library: false` keeps the voice with this recording only: it speaks for that speaker here, is not offered in other transcriptions (`library` is false on the voice), and goes when the recording does; PATCH `library: true` adds it later. The default `true` saves it to the library. `draft: true` makes a draft to try first (no `consent` or `name` needed; preview it in any language): it neither names nor stands for the speaker until it is kept with PATCH /v1/voices/{id} `{"draft": false, "consent": true, "name": …}`, which links it to the speaker; DELETE drops it.
  • GET /v1/voices/preview Preview a voice
    A few seconds of a voice speaking a language, Ogg/Opus (`format=caf` for Apple's media stack). Only voices the catalogue offers can be previewed.
  • POST /v1/voices/restore Restore voices
    Restore several voices in one call (up to 500 `ids`), each as the one-voice call would. The answer lists the ids that were done and, for each one that was not, why (`failed[].code` and `message`); one failure does not stop the others.
  • GET /v1/voices/{id} Get a voice
    Training state: `training` / `ready` / `failed`, with `progress` and, when it failed, `error`.
  • PATCH /v1/voices/{id} Update a voice
    Rename a voice, recolour it, or switch it off. Absent fields are left alone; `color: ""` clears a chosen colour. `library: true` adds a voice kept with its recording only (or an automatic clone, with `consent: true`) to the voice library, so it is offered in every transcription from then on; `library: false` takes one made from a recording's speaker back out (it stays with that recording and goes with it). Not the place to withdraw permission — that deletes the voice.
  • DELETE /v1/voices/{id} Delete a voice
    Delete the voice, its recorded sample and anything trained from it.
  • POST /v1/voices/{id}/archive Archive a voice
    Put a voice away: it no longer speaks, is not offered, and no longer names or stands for any speaker — speakers it was linked to are unlinked, and those it had named are back to their plain labels, so they can be cloned afresh. Its sample is kept; restore brings it back (speakers stay unlinked) and delete removes it for good. `archivedAt` says when.
  • POST /v1/voices/{id}/consent Grant or refuse consent for a voice
    Record whether the speaker agreed to their cloned voice. Every speaker with 20 seconds of speech in a recording is cloned automatically with `consent: "pending"`; a clone speaks, is offered in `GET /v1/voices/available` and has a preview only once its consent is `granted`. `granted` (optionally with a `name`, which also names the speaker in the recording it came from) makes it usable; `refused` deletes its audio and voice at once and it is not cloned again. Refusing later withdraws consent the same way. Each decision is recorded with who made it and when; `granted` on a voice already granted is recorded again (the host re-confirming permission). With `settings.voiceCloning` on, a transcription's automatic clones are granted on the host's word as they are made.
  • GET /v1/voices/{id}/preview Preview a cloned voice
    The cloned voice speaking, Ogg/Opus — `format=caf` for Apple's media stack, which will not play Ogg. 409 while it is not `ready`. The owner's token may ride in `access_token`, because an `<audio>` element cannot carry a header.
  • POST /v1/voices/{id}/restore Restore a voice
    Bring an archived voice back into use. Speakers it was unlinked from when it was archived stay unlinked.
  • GET /v1/voices/{id}/sample Get a voice's sample
    The audio this voice was made from, for its owner alone. Ranged. It is what lets a draft picked up after a reload be the screen that was left — take, waveform and playback — rather than a preview of one.