Skip to main content

Developers

Speech to text API for recorded audio

Use Hushscript's speech to text API to upload recorded audio, check the job, and read the transcript with a scoped token.

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.

  1. Read the account. GET /v1/account. If you need to buy minutes and card_on_file is false, save a card first.

  2. Save a card, once. POST /v1/billing/setup-session returns a checkout_url, a session_id, and an expires_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/cards lists it with its id. POST /v1/billing/cards/default switches the default if the account holds several.

  3. Buy minutes. GET /v1/billing/packs lists the five packs with id, seconds, amount, and currency:

    curl https://api.hushscript.com/v1/billing/packs \
      -H "Authorization: Bearer hsr1:eu:pat.7f3a9c.2b6e1d4a9f2c88b1a3d4e5f60718293"

    POST /v1/billing/purchase with pack, quick_payment_method_id (the saved card’s id), and an idempotency_key charges the card off-session and answers the new balance_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 billing scope, billing enabled on the token, and billing enabled for the account. A body without a card id, or a declined card, is refused with 4021 pat_purchase_requires_app; a card the issuer wants to challenge is refused with 4023 pat_purchase_requires_authentication. Neither opens a page nobody can finish.

  4. Upload. POST /v1/uploads with size_bytes, duration_seconds (15 seconds to 10 hours), an optional title, transcription_options, matter_code, and an idempotency_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_code is 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, with org_matter_code_required. client_code is 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_id plus 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=..." }
      ]
    }

    PUT each 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.bin

    Fetch further pages from GET /v1/uploads/{job}/part-urls?start=<n>, and call POST /v1/uploads/{job}/heartbeat well 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 repeated PUT replaces 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.

  5. Start and wait. POST /v1/uploads/{job}/complete with the n and etag of every part you sent to a presigned URL (omit parts when 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}: state moves through uploading, queued, and processing to done or failed, and a finished job carries a transcript_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"
    }
  6. 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/transcripts pages 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.

Start transcribing with 30 free minutes

Preview up to 5 minutes before you create an account, and a new personal account gets a free preview of its first upload in the app. Full transcription needs an account and minutes; eligible personal accounts get 30 free minutes once, after card verification or with a first purchase. No subscription.

Get 30 free minutes