REST API · v1

Transcripton API

Drop a file or a URL into our pipeline and get a transcript back — with speaker labels, AI summary, multi-language translation, and seven download formats. Same engine that powers the dashboard, just wired up for your scripts and apps.

Plan gate. The API is part of the Pro and Business plans. Free accounts can sign up and explore the dashboard, but POST /api-keys returns 403 until you upgrade.

Introduction

All endpoints live under a single base URL. Every response is JSON unless you explicitly request another format (transcript exports return TXT, SRT, VTT, DOCX, MD, or PDF).

Base URL
https://api.transcription.solutions/api/v1

Requests use standard HTTP verbs and status codes. Successful calls return 2xx with a JSON body; failures return 4xx or 5xx with { "detail": { "code": "...", "message": "..." } }. See Errors for the full code list.

Authentication

Sign in to app.transcription.solutions/settings API keys, click Create, give the key a name, and copy the value. We store only its SHA-256 hash, so the raw ts_...token is shown exactly once. Lose it and you create a new one — there's no recovery flow on purpose.

Pass the key in an X-API-Key header on every call:

curl
curl https://api.transcription.solutions/api/v1/jobs \
  -H "X-API-Key: ts_••••••••••••••••••••••••••••••••"
Treat keys like passwords. Never commit them, never ship them in a mobile binary, never put them in a frontend bundle. If a key leaks, delete it from Settings → API keys — the next request with that hash returns 401.

Quickstart

Three calls, end to end: upload a file, poll until it's done, download the transcript.

bash
# 1) Upload — returns { id, status }
JOB=$(curl -sS -X POST https://api.transcription.solutions/api/v1/jobs \
  -H "X-API-Key: $TS_KEY" \
  -F "file=@meeting.mp3" \
  -F "diarize=true" | jq -r .id)

# 2) Poll — repeat every few seconds, or use the WebSocket
while true; do
  STATUS=$(curl -sS https://api.transcription.solutions/api/v1/jobs/$JOB \
    -H "X-API-Key: $TS_KEY" | jq -r .status)
  [ "$STATUS" = "done" ] && break
  [ "$STATUS" = "failed" ] && echo "failed" && exit 1
  sleep 3
done

# 3) Download — TXT, SRT, VTT, DOCX, MD, JSON, PDF
curl -sS "https://api.transcription.solutions/api/v1/jobs/$JOB/export?format=md" \
  -H "X-API-Key: $TS_KEY" -o meeting.md

Plans & limits

Limits are enforced per-user, not per-key. All keys on a Pro account share that account's monthly minute pool.

CapabilityFreeProBusiness
API access
Minutes / month606002,500
Max file duration30 min60 min4 h
Max file size100 MB500 MB2 GB
Concurrent jobs135
Speaker diarization
AI summary & polish
Translations / month1050
Queue priorityLowNormalHigh

Need more? Email us for an Enterprise quote — custom minutes, SLAs, dedicated queue, and on-prem deployment options.

Upload a file

POST/api/v1/jobs

Multipart upload. The file streams straight to S3 — never buffered in RAM — and a magic-byte check on the first 16 KB rejects polyglots before they reach ffmpeg.

Form fields

FieldTypeDescription
filefileAudio or video. mp3, m4a, wav, ogg, opus, flac, webm, mp4, mov, mkv, avi.
diarizebooleanSpeaker labels. Defaults to false. Pro+ only.
curl
curl -X POST https://api.transcription.solutions/api/v1/jobs \
  -H "X-API-Key: $TS_KEY" \
  -F "file=@meeting.mp3" \
  -F "diarize=true"
json · 200
{
  "id": "5d3f2c80-9a4f-4f3a-9ab1-5d7c3a1e0b91",
  "status": "queued"
}

Submit a URL

POST/api/v1/jobs/from-url

Hand us any link our universal extractor recognises — YouTube, TikTok, Instagram, Twitter/X, Facebook, Reddit, Vimeo, SoundCloud, Twitch, podcast feeds, plus ~1,500 more sites — or a direct CDN URL to an audio/video file. We download, transcode, and queue.

curl
curl -X POST https://api.transcription.solutions/api/v1/jobs/from-url \
  -H "X-API-Key: $TS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "language": "auto",
    "diarize": false
  }'
json · 200
{
  "id": "5d3f2c80-9a4f-4f3a-9ab1-5d7c3a1e0b91",
  "status": "queued"
}
Cost-aware routing. YouTube uses our paid Apify backend for reliability; everything else goes through yt-dlp first and falls back to a direct GET. You pay only for transcribed minutes — extraction is on us.

List jobs

GET/api/v1/jobs

Paginated list of the calling user's jobs, newest first. Optional status filter accepts any of queued, downloading, extracting, transcribing, diarizing, analyzing, done, failed.

curl
curl "https://api.transcription.solutions/api/v1/jobs?page=1&limit=20&status=done" \
  -H "X-API-Key: $TS_KEY"
json · 200
{
  "jobs": [
    {
      "id": "5d3f2c80-...",
      "status": "done",
      "stage": 6,
      "progress": 100,
      "duration_sec": 312.4,
      "language_detected": "en",
      "created_at": "2026-05-04T08:12:31Z"
    }
  ],
  "total": 47,
  "page": 1,
  "limit": 20
}

Get a job

GET/api/v1/jobs/{id}

Full job record with transcription text, optional diarization segments, summary, and any cached translations. Poll this every 2–5 seconds, or — much better — subscribe to WebSocket updates.

curl
curl https://api.transcription.solutions/api/v1/jobs/$JOB_ID \
  -H "X-API-Key: $TS_KEY"
json · 200
{
  "id": "5d3f2c80-...",
  "status": "done",
  "stage": 6,
  "progress": 100,
  "duration_sec": 312.4,
  "transcription": {
    "id": "...",
    "full_text": "Hello, welcome to the show...",
    "language_detected": "en"
  },
  "diarization": {
    "segments": [
      { "speaker": "speaker_0", "start": 0.0, "end": 4.2,
        "text": "Hello, welcome to the show." }
    ],
    "num_speakers": 2
  },
  "summary": null,
  "translations": []
}

Export a transcript

GET/api/v1/jobs/{id}/export?format=...

Seven formats. JSON returns the versioned TranscriptionExportV1 payload (stable across point releases — breaking changes bump the version).

FormatUse it for
txtPlain transcript, no metadata.
mdSpeaker-labelled paragraphs, summary, action items.
srtSubtitles for video editing.
vttWeb-native captions (HTML5 video).
docxWord — meeting notes, speaker turns.
pdfPrint-ready report with summary block.
jsonStructured payload — pipe into your own tooling.
curl
curl "https://api.transcription.solutions/api/v1/jobs/$JOB_ID/export?format=docx" \
  -H "X-API-Key: $TS_KEY" \
  -o meeting.docx

Content-Disposition uses RFC 5987 encoding so non-ASCII filenames (Cyrillic, CJK, emoji) survive a curl -OJ round-trip without mojibake.

Delete a job

DELETE/api/v1/jobs/{id}

Removes the source file from S3 and the row from Postgres. If the job is still in flight, we revoke the Celery task — Whisper stops mid-transcription. Any minutes already billed are refunded via a negative UsageRecord so the audit ledger stays append-only.

curl
curl -X DELETE https://api.transcription.solutions/api/v1/jobs/$JOB_ID \
  -H "X-API-Key: $TS_KEY"

Polish — paragraph breaks & punctuation

POST/api/v1/jobs/{id}/polish

Whisper output is one wall of text. /polish re-flows it into readable paragraphs and fixes punctuation — without rewriting words. A drift guard rejects any LLM response whose character count moves more than ±15 % from the source, so you keep your original text.

Idempotent — if the transcript already has paragraph breaks the cached version is returned without spending another LLM call.

curl
curl -X POST https://api.transcription.solutions/api/v1/jobs/$JOB_ID/polish \
  -H "X-API-Key: $TS_KEY"
json · 200
{ "polished": true, "cached": false, "paragraphs": 8, "chars": 2147 }

Summarize

POST/api/v1/jobs/{id}/summarize

Generates an executive summary, key points, and action items from a finished transcript. One LLM round-trip; idempotent — a repeat call returns the cached summary object without re-billing.

curl
curl -X POST https://api.transcription.solutions/api/v1/jobs/$JOB_ID/summarize \
  -H "X-API-Key: $TS_KEY"
json · 200
{
  "summarized": true,
  "cached": false,
  "summary": "Quarterly review covered Q3 launch, channel mix, and Q4 hiring plan...",
  "key_points": [
    "Q3 closed 18% above target",
    "Paid social CAC dropped from $42 to $31"
  ],
  "action_items": [
    "Marketing — finalise Q4 budget by Friday",
    "Eng — open backend headcount by next Monday"
  ]
}

Translate a transcript

POST/api/v1/transcriptions/{id}/translate

Pass the transcription.id from GET /jobs/{id} and the target ISO 639-1 code. Translation is idempotent per (transcription, target_lang) — a repeat call returns the cached row and does not bump your translations counter.

curl
curl -X POST https://api.transcription.solutions/api/v1/transcriptions/$TR_ID/translate \
  -H "X-API-Key: $TS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "target_lang": "es" }'
json · 200
{
  "id": "...",
  "transcription_id": "...",
  "target_lang": "es",
  "text": "Hola, bienvenido al programa...",
  "llm_model": "gpt-5-mini",
  "created_at": "2026-05-04T08:14:09Z"
}

List all cached translations for a transcription with GET /transcriptions/{id}/translations.

WebSocket — realtime progress

GETwss://api.transcription.solutions/api/v1/ws

Skip the polling loop. Connect, send an auth frame with your Supabase JWT (the dashboard's session token — API keys are HTTP-only by design), then subscribe to a job ID and receive stage updates as the worker bumps progress.

js
const ws = new WebSocket("wss://api.transcription.solutions/api/v1/ws");

ws.onopen = () => {
  ws.send(JSON.stringify({ type: "auth", token: SUPABASE_JWT }));
  ws.send(JSON.stringify({ subscribe: jobId }));
};

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  // { stage, progress, status }
  if (msg.status === "done") fetchJob(jobId);
};

Webhooks

Have us POST to your endpoint when a job finishes — no polling, no sockets. Configure delivery URLs at Settings → Webhooks. Each event is signed:X-Webhook-Signature is HMAC-SHA-256 over the raw body using the secret shown in the dashboard.

json · payload
{
  "event": "job.done",
  "data": {
    "job_id": "5d3f2c80-...",
    "status": "done",
    "duration_sec": 312.4
  },
  "timestamp": 1746345210
}

Verify in your handler before you trust anything in the body:

node
import crypto from "node:crypto";

function verify(req, secret) {
  const sig = req.headers["x-webhook-signature"];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(req.rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(sig, "hex"),
    Buffer.from(expected, "hex")
  );
}

Failed deliveries retry on a backoff schedule — 30 s → 5 min → 30 min → 1 h → 4 h — then cancel. Inspect attempts at GET /integrations/webhooks/deliveries.

Errors

Every error response is shaped the same way:

json
{
  "detail": {
    "code": "quota_exceeded",
    "message": "You've used 600.0 of your 600 minutes this period. Resets May 14. Upgrade to keep transcribing.",
    "minutes_used": 600.0,
    "minutes_limit": 600,
    "plan": "pro"
  }
}
HTTPCodeMeaning
401invalid_api_keyMissing or unknown X-API-Key header.
403email_not_verifiedFree signup hasn't confirmed their email yet.
403polish_requires_paid_plan/polish on a Free plan.
403summarize_requires_paid_plan/summarize on a Free plan.
403translation_not_in_planTranslations gated to Pro+.
402quota_exceededMonthly minute pool is empty until reset_at.
402translation_quota_exceededMonthly translation cap hit.
413file_too_largeFile exceeds max_file_size_mb for the plan.
413file_too_longProbed duration exceeds max_file_minutes.
400unsupported_formatExtension or magic bytes don't match a known media type.
400unsupported_languageTranslation target outside our supported set.
404not_foundJob or transcription doesn't exist (or isn't yours).
409job_not_readyPolish/summarize called before transcription finished.
429rate_limitedPer-IP burst cap. Honour the Retry-After header.
503asr_quota_exceededWhisper provider returned insufficient_quota. Transient.
503translation_unavailableLLM key is missing or rotating. Transient.

Rate limits

Per-key burst limits track your plan. Hitting the wall returns 429 with a Retry-After header (seconds). For high-throughput integrations, contact support for a custom extended tier.

EndpointFreePro / Business
POST /jobs & POST /jobs/from-url3 / min10 / min
GET /jobs/{id}60 / min120 / min
POST /jobs/{id}/polish & /summarize20 / min

Versioning

The current API is v1. Backwards-incompatible changes will introduce v2 at a different path — existing integrations on v1 keep working. Additive changes (new fields on responses, new optional inputs, new error codes) ship without bumping the version. Pin to specific JSON shapes by parsing only the fields you need.

Deprecations are announced via the changelog with at least 90 days notice and a sunset header on responses.

Support

Bug reports, integration help, feature requests: support@transcription.solutions. We answer every email; if you're building something we haven't shipped yet, tell us — the obvious wins ship fast.

Ready to build?

Sign up, upgrade to Pro, and create your first key from Settings → API keys.

Create an account →