Hushscript also answers plain REST calls, for agents and scripts that would rather call an HTTP endpoint than run an MCP client. It is the same account, the same balance, and the same bearer tokens throughout: nothing here changes how transcription is priced or stored.
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 with no person involved: 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:
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": 3,
"max_daily_seconds": 36000,
"active_jobs": 0,
"daily_seconds_used": 0
}
}
That answers the balance in seconds, the seconds held by running jobs, the
credit expiry date, whether a card is saved, the data region, and the
admission limits (max_active_jobs, max_daily_seconds, and what is used).
A fresh account starts at a zero balance. POST /v1/uploads checks the
balance, not the card: without minutes it returns 3001 insufficient_balance.
The card is how the balance gets there, since the one-off card verification
in the app releases the 30 free minutes, and packs are bought against a saved
card.
Automating end to end
Everything after the account exists runs without a person, whether a person
opened that account in the app or an agent opened it through
/v1/agent/accounts (see Automated usage). A single browser
step remains, and an agent may drive that browser itself.
-
Read the account.
GET /v1/account. Ifcard_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 on the token and billing switched on 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, 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" }'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. Audio only: mp3, m4a, wav, flac, ogg, or opus, at most 5 GB, probed on the server; extract the audio track from a video 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 |
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 until the account owner switches billing on for
the account in the app; a billing call on an account where that switch is off
fails with the same pat_scope_missing, and re-minting the token does not
clear it. A token cannot create, list, or revoke tokens, and an account holds
at most 25 active ones.
Endpoints
Live today, each 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 |
none |
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/members |
org:read |
GET |
/v1/org/invoices |
org:read |
GET |
/v1/org/ledger |
org:read |
GET |
/v1/org/audit |
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 revokes it, 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 51 tools instead of these endpoints, for a client that already speaks MCP rather than REST.
Limits
Requests made with a token are limited to 1,200 a minute per token.
POST /v1/billing/setup-session and POST /v1/billing/purchase share a
tighter budget of 60 a minute per account, ahead of the Stripe call they would
make. A refused request answers 429 rate_limited with a Retry-After header
naming the wait in seconds; back off for that long before retrying. Uploads
are capped at 5 GB and 10 hours, with a 15 second minimum, and the account’s
own admission limits (max_active_jobs, max_daily_seconds) are reported by
GET /v1/account.
Sandbox
There is no dry-run mode: every /v1/ call runs against a real account, and
buying minutes spends real money.
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 | 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.