The Hushscript speech to text API accepts recorded audio through /v1/, then
lets your script poll the job and read the transcript. It is an online
transcription workflow, not a live streaming or raw recognition-model endpoint.
The MCP server uses the same account, balance, and bearer token when
your client prefers tools to HTTP calls.
For TypeScript, Python, Go, PHP, or a terminal, the SDKs and CLI wrap these calls so one call uploads a file and returns the transcript.
Audio and video transcription through the API
The audio transcription API accepts prepared audio files, including recorded voice. For an STT (speech-to-text) integration, submit recorded files; live microphone streaming is unavailable. For video transcription, upload an MP4 or MOV file with an audio track as it is: the server reads the audio track and skips the video. Extract the audio from any other video format first; the API does not accept a video URL. The browser workflow is explained at video to text.
Quickstart
The API needs a personal access token, and there are two ways to hold one. A
person creates one under Tokens on the MCP page in the account,
picks its scopes, and decides whether it may buy minutes. Or an agent opens
its own account without a human account holder: POST /v1/agent/accounts starts a
paid signup, and POST /v1/agent/accounts/{signup_id}/claim exchanges its
one-time claim secret for a real account and a first token. Either way the
token is shown once and looks like hsr1:<region>:pat.<id>.<secret>. A token
minted through the MCP server’s OAuth sign-in works here too; see
Connect. A token can never mint, list, or revoke other
tokens, and an account holds at most 25 active ones; the full agent-account
flow, including credential rotation, is the Automated usage
walkthrough.
Once you hold a token, call the API directly. This example represents a new personal account before card verification or a purchase; an agent-created account starts with the minute pack it paid for:
curl https://api.hushscript.com/v1/account \
-H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"
{
"balance_seconds": 0,
"held_seconds": 0,
"credits_expire_at": null,
"card_on_file": false,
"data_region": "eu",
"limits": {
"max_active_jobs": 50,
"max_daily_seconds": 144000,
"active_jobs": 0,
"used_today_seconds": 0
}
}
That answers the balance in seconds, the seconds held by running jobs, the
credits_expire_at (always null because minutes do not expire), whether a card is saved, the data region, and the
admission limits (max_active_jobs, max_daily_seconds, and what is used).
The daily limit is the account’s current effective limit, not a value to
hardcode. POST /v1/uploads checks available minutes, not the card: without
enough it returns 3001 insufficient_balance.
Is the speech to text API free?
There is no general free API tier, but you can try the workflow before you write any code: the public preview transcribes up to five minutes from one recording before sign-in. An eligible personal account then gets 30 free minutes after card verification (see pricing for how that works); an agent-created account skips the welcome minutes and starts with the pack it buys at signup. A full API job needs an account and minutes either way.
Automating end to end
The same /v1/ calls work with a token created by a person in the app or one
claimed by an agent after paid signup (see Automated usage).
Saving a card requires hosted Checkout in a browser; an agent may drive it.
Later off-session purchases can still be refused if the card issuer requires
authentication.
-
Read the account.
GET /v1/account. If you need to buy minutes andcard_on_fileis false, save a card first. -
Save a card, once.
POST /v1/billing/setup-sessionreturns acheckout_url, asession_id, and anexpires_at:curl -X POST https://api.hushscript.com/v1/billing/setup-session \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"{ "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_a1B2c3D4e5F6g7H8", "session_id": "seti_1PxYzQ2eZvKYlo2C", "expires_at": "2026-09-11T15:45:00Z" }Open the URL in a browser you control and complete the hosted Stripe page; nothing is charged and Hushscript never sees the card. Then
GET /v1/billing/cardslists it with its id.POST /v1/billing/cards/defaultswitches the default if the account holds several. -
Buy minutes.
GET /v1/billing/packslists the five packs withid,seconds,amount, andcurrency:curl https://api.hushscript.com/v1/billing/packs \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"POST /v1/billing/purchasewithpack,quick_payment_method_id(the saved card’s id), and anidempotency_keycharges the card off-session and answers the newbalance_seconds:curl -X POST https://api.hushscript.com/v1/billing/purchase \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293" \ -H "Content-Type: application/json" \ -d '{ "pack": "300min", "quick_payment_method_id": "pm_2QeT4bY7uX9mK3nP", "idempotency_key": "purchase-2026-09-11-02" }'{ "balance_seconds": 18000 }This needs the
billingscope, billing enabled on the token, and billing enabled for the account. A body without a card id, or a declined card, is refused with4021 pat_purchase_requires_app; a card the issuer wants to challenge is refused with4023 pat_purchase_requires_authentication. Neither opens a page nobody can finish. -
Upload.
POST /v1/uploadswithsize_bytes,duration_seconds(15 seconds to 10 hours), an optionaltitle,transcription_options,matter_code, and anidempotency_key:curl -X POST https://api.hushscript.com/v1/uploads \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293" \ -H "Content-Type: application/json" \ -d '{ "size_bytes": 41943040, "duration_seconds": 1200, "title": "Q3 board call", "transcription_options": { "speaker_detection": true }, "idempotency_key": "upload-2026-09-11-02" }'matter_codeis free text of at most 64 characters, and it only means something for a token that belongs to a workspace: it is shown against the job in that workspace’s ledger and usage reports. A workspace whose matter policy is set to required refuses an upload that arrives without one, withorg_matter_code_required.client_codeis accepted and ignored, because the client is an attribute of the matter, set by a workspace admin.It places a credit hold and answers a
job_idplus a first page of presigned part URLs:{ "job_id": "job_5e2a91cf4d7b6081a9f3c2e4b5d6a7c8", "part_urls": [ { "n": 1, "url": "https://r2.hushscript.com/uploads/job_5e2a91cf.../part-1?X-Amz-Signature=..." } ] }PUTeach part to its URL (32 MiB parts by default, the last one smaller):curl -X PUT "https://r2.hushscript.com/uploads/job_5e2a91cf.../part-1?X-Amz-Signature=..." \ --data-binary @part-01.binFetch further pages from
GET /v1/uploads/{job}/part-urls?start=<n>, and callPOST /v1/uploads/{job}/heartbeatwell inside the 20 minute idle timeout during a long transfer. When presigned URLs are unavailable, or one part fails,PUT /v1/uploads/{job}/parts/{n}sends that part’s bytes through the API instead; a repeatedPUTreplaces the part, so retries are safe. Supported: mp3, m4a, mp4 or mov (the audio track is used, video tracks are skipped), wav, flac, ogg, or opus, probed on the server; extract the audio from any other video format before uploading. -
Start and wait.
POST /v1/uploads/{job}/completewith thenandetagof every part you sent to a presigned URL (omitpartswhen every part went through the API):curl -X POST https://api.hushscript.com/v1/uploads/job_5e2a91cf4d7b6081a9f3c2e4b5d6a7c8/complete \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293" \ -H "Content-Type: application/json" \ -d '{ "parts": [ { "n": 1, "etag": "\"9f86d081884c7d65\"" } ] }'Then poll
GET /v1/jobs/{id}:statemoves throughuploading,queued, andprocessingtodoneorfailed, and a finished job carries atranscript_id:curl https://api.hushscript.com/v1/jobs/job_5e2a91cf4d7b6081a9f3c2e4b5d6a7c8 \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"{ "id": "job_5e2a91cf4d7b6081a9f3c2e4b5d6a7c8", "state": "done", "transcript_id": "trs_2b6e1d4a9f7c3088a1b2c3d4e5f6a7b9" } -
Read the result.
GET /v1/transcripts/{id}returns the metadata and the full body, with speaker names, the detected language, tags, and the auto-delete date:curl https://api.hushscript.com/v1/transcripts/trs_2b6e1d4a9f7c3088a1b2c3d4e5f6a7b9 \ -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"GET /v1/transcriptspages the list.
Transcription options are the app’s, verbatim: language_code or
auto-detection, speaker_detection, medical_mode (needs language_code in
en, es, fr, or de, billed at +100%), translation_targets (+25% per
language), multichannel, keyterms_prompt, a free-text prompt, and the
saved dictionary_ids and prompt_preset_id that
GET /v1/transcription-context lists.
Some of the account is still MCP-only. Importing from a link, editing or
deleting transcripts, Insights, exports, saving dictionaries and prompt
presets, and account settings have no /v1/ route yet; the MCP server
covers all of them with the same token.
Authentication
Every /v1/ call carries a bearer token in the Authorization header,
checked against the same scopes the MCP server uses:
| Scope | What it allows |
|---|---|
transcripts:read |
Read and export transcripts, their metadata, and the deleted list |
transcripts:write |
Edit, tag, translate, restore, and delete transcripts |
transcribe |
Upload recordings, import from a link, and run transcriptions |
insights |
Read, generate, and delete Insights |
context |
Manage saved dictionaries and prompt presets |
account:read |
Read balance, usage, transactions, and holds |
settings:write |
Change account settings |
org:read |
Read workspace members, ledger, invoices, and audit log |
org:write |
Change member rights, schedule the move to firm (permanent) custody, and place or lift legal holds |
export |
Request a full account export, check its status, and download it |
billing |
Save a card and buy minute packs |
A call outside the token’s scopes fails with pat_scope_missing, at 403. The
billing scope is also inert unless billing is enabled both on the token and
for the account. A billing call with either switch off fails with the same
pat_scope_missing; re-minting the token does not turn on the account switch.
A token cannot create, list, or revoke tokens, and an account holds at most 25
active ones.
These 11 scopes are caller-selectable. pat:rotate is a separate scope granted
to an agent account’s first token and its rotated successors; it lets that
token replace itself, not manage other tokens. You cannot select it when
creating a token in the app.
Endpoints
The production OpenAPI document lists 38 method/path operations over 37 paths. Each operation below is gated by the scope shown:
| Method | Path | Scope |
|---|---|---|
POST |
/v1/agent/accounts |
none |
POST |
/v1/agent/accounts/{signup_id}/claim |
none |
POST |
/v1/agent/credentials/rotate |
pat:rotate |
GET |
/v1/account |
account:read |
POST |
/v1/billing/setup-session |
billing |
GET |
/v1/billing/cards |
billing |
POST |
/v1/billing/cards/default |
billing |
GET |
/v1/billing/packs |
billing |
POST |
/v1/billing/purchase |
billing |
GET |
/v1/billing/transactions |
account:read |
GET |
/v1/billing/ledger |
account:read |
GET |
/v1/billing/holds |
account:read |
POST |
/v1/uploads |
transcribe |
GET |
/v1/uploads/{job}/part-urls |
transcribe |
PUT |
/v1/uploads/{job}/parts/{n} |
transcribe |
POST |
/v1/uploads/{job}/heartbeat |
transcribe |
POST |
/v1/uploads/{job}/complete |
transcribe |
GET |
/v1/jobs |
transcribe |
GET |
/v1/jobs/{id} |
transcribe |
GET |
/v1/transcripts |
transcripts:read |
GET |
/v1/transcripts/deleted |
transcripts:read |
GET |
/v1/transcripts/{id} |
transcripts:read |
GET |
/v1/transcription-context |
context |
GET |
/v1/org |
org:read |
GET |
/v1/org/members |
org:read |
PATCH |
/v1/org/members/{user_id} |
org:write |
GET |
/v1/org/matters |
org:read |
GET |
/v1/org/transcripts |
org:read |
POST |
/v1/org/transcripts/{id}/hold |
org:write |
DELETE |
/v1/org/transcripts/{id}/hold |
org:write |
POST |
/v1/org/custody |
org:write |
GET |
/v1/org/invoices |
org:read |
GET |
/v1/org/ledger |
org:read |
GET |
/v1/org/ledger.csv |
org:read |
GET |
/v1/org/usage |
org:read |
GET |
/v1/org/usage.csv |
org:read |
GET |
/v1/org/audit |
org:read |
GET |
/v1/org/audit.csv |
org:read |
/v1/agent/accounts opens a headless signup with no token yet,
/v1/agent/accounts/{signup_id}/claim turns a paid signup into a real
account and its first token, and /v1/agent/credentials/rotate mints a
successor for the calling token and retires it once the successor is first
used, touching no other credential on the account. The Automated usage walkthrough covers all
three end to end.
Every list takes a limit and an opaque cursor, and answers a
next_cursor that is null on the last page. Pass the cursor back unchanged;
it encodes the position, so a page is never skipped or repeated when rows
arrive between calls.
The full, current definition, with parameters and response schemas for every operation, is the OpenAPI document: treat it as the source of truth over this table. It is served to any bearer token regardless of scope, so a client can fetch it with the credentials it already holds.
MCP interface
Prefer tools over raw HTTP? The MCP server puts the same account behind 56 tools instead of these endpoints, for a client that already speaks MCP rather than REST. It advertises only the tools the connected token’s scopes allow.
Limits
Requests made with a human account’s token are limited to 1,200 a minute per
token. An agent account’s rotated tokens share that budget across the account.
Stripe-calling billing routes share a tighter 60 a minute per account for PAT
callers: setup session, purchase, default-card changes, card removal, and
card-list cache misses. Ordinary throttling answers 429 rate_limited with
Retry-After in seconds; wait that long before retrying. Purchases by an agent
account or by a human account’s PAT share a cap of 3 attempts per rolling 24
hours per account. The agent’s signup purchase counts; declines or errors after
reservation count too. A human browser-session purchase is exempt. A cap refusal
answers HTTP 429 with 4038 agent_purchase_capped and Retry-After.
Recordings can run from 15 seconds to 10 hours. GET /v1/account reports the
account’s current admission limits (max_active_jobs, max_daily_seconds).
Sandbox
There is no general transcription sandbox: ordinary /v1/ calls run against
real accounts, and purchases spend real money. The exception is signup
validation: POST /v1/agent/accounts accepts dry_run: true, returns a
non-claimable dry_ signup id, and opens no Stripe session or account.
Errors
An error is a JSON body carrying a stable numeric code and an error slug,
at the HTTP status shown below. /v1/ error bodies are a frozen projection:
error, code, and a fixed set of detail keys. /api’s error shape is not
frozen and may carry more. The codes an automated caller must handle:
| Status | Code | Slug | Meaning |
|---|---|---|---|
| 401 | 2000 | unauthorized |
Missing or invalid bearer token |
| 403 | 2022 | pat_forbidden |
The route accepts no token at all |
| 403 | 2023 | pat_scope_missing |
The token lacks the scope this call needs, or billing is off for the account |
| 403 | 4021 | pat_purchase_requires_app |
No saved card was named, or the charge was declined; nothing was charged |
| 403 | 4023 | pat_purchase_requires_authentication |
The card issuer wants a challenge the caller cannot complete; the card itself is fine |
| 402 | 3001 | insufficient_balance |
Not enough balance to admit the job |
| 402 | 3002 | card_declined |
The saved card was declined |
| 402 | 3003 | org_insufficient_balance |
The workspace pool funding this upload is short |
| 404 | 4000 | not_found |
No such job, upload, or resource |
| 409 | 4001 | wrong_state |
The resource is not in a state that allows this |
| 422 | 1021 | duration_out_of_range |
The declared duration is under 15 seconds or over 10 hours |
| 429 | 4038 | agent_purchase_capped |
The account has used its token purchase-attempt allowance; wait for Retry-After |
| 429 | 5000 | rate_limited |
Too many requests; wait for Retry-After |
| 502 | 6001 | provider_error |
An upstream service failed |
| 500 | 9000 | internal_error |
An unexpected server error |
The OpenAPI document names the exact code every operation can return, and the
MCP server publishes the whole catalog as the hushscript://error-codes
resource.