Skip to main content

AI features

Hushscript developer API

A REST surface built for AI agents and automation, over the same account, the same balance, and the same tokens as everything else.

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.

  1. Read the account. GET /v1/account. If 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 on the token and billing switched on 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, 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"
      }'

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

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

Start transcribing – 30 minutes to try

A $1 hold confirms your card and releases immediately — you're never charged, and 30 free minutes land right away.

Start – 30 free minutes