Specala AIDocs
API Reference

Upload a file

POST /transcriptions — upload an audio or video file (up to 2 GB) for transcription with one HTTP request, then poll until it's completed.

POST /transcriptions

Uploads one audio/video file as a raw request body and starts transcription. Returns immediately with a transcription id — poll Get transcription until status is completed.

Requires upload permission

Only keys created with Allow file uploads enabled can call this endpoint. Existing keys are read-only — create a new key if yours predates uploads. See Authentication → Key permissions.

Request format

The file is the request body — no multipart forms, no JSON wrapping, no chunk protocol:

PartValue
BodyThe file bytes, as-is (--data-binary in curl).
AuthorizationBearer sk_live_… (key with upload permission).
Content-LengthRequired. HTTP clients set it automatically for file bodies; requests without it are rejected with 411.
Content-DispositionRequired: attachment; filename="meeting.mp3". The extension determines the accepted format; the name becomes the transcription title.
Idempotency-KeyOptional but recommended: a unique string per file (e.g. a UUID). Makes retries safe — see Retries.

Why the filename lives in a header

Filenames often contain personal data. Headers stay out of URL logs — that's why ?filename= is not accepted. For non-ASCII names (accents, Turkish characters, any non-Latin script) use the standard RFC 5987 form: Content-Disposition: attachment; filename*=UTF-8''Reuni%C3%B3n.mp3 (decodes to Reunión.mp3).

Query parameters

All optional:

ParameterTypeDescription
languagestringSpoken language. Default auto (detected from the audio) — see Supported languages.
project_idUUIDAttach the transcription to a project. Get UUIDs from List projects.
diarizebooleanDetect speakers. Default true.
speakers_countintegerNumber of speakers (120), if known — improves diarization.
ai_metadatabooleanGenerate the AI layer: summary, title, speaker names, topics and default AI reports. Default false — see below.

AI analysis is off by default for API uploads

With ai_metadata=false (the default) you get the full transcript faster: text with speaker labels, but summary and topics stay null, prompt_results is empty, the title is the filename, and speakers keep generic labels. Pass ai_metadata=true when you want the same AI layer the app produces. Either way the transcription shows up in the app, where AI reports can still be generated on demand later. Billing is identical in both modes.

Supported languages

auto (default — the language is detected from the audio) or one of:

en, es, ru, zh, hi, ar, pt, bn, fr, de, ja, ko, tr, it, vi, pl, uk, nl, id, th, fa, sv, cs, ro, el, hu, da, fi, no, sk, he, ms, bg, hr, sr, sl, lt, lv, et, ta, te, mr, ur, sw, ml, kn, gu, pa, my, ne, si, km, lo, az, kk, uz, ka, hy, sq, bs, mk, is, mn, tg, tk, tt, ca, eu, gl, cy, af, tl, jw, su, yo, ha, so, am, sn, mg, ln, mt, lb, oc, br, nn, fo, sd, ps, ht, mi, haw, yi, ba, be, as, bo, sa, la

Two-letter ISO 639-1 codes (plus haw and jw). An unsupported value returns 422.

Request

curl -X POST "https://app.specala.ai/api/v1/developer/transcriptions?language=en" \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H 'Content-Disposition: attachment; filename="standup.mp3"' \
  --data-binary @standup.mp3
import httpx

with open("standup.mp3", "rb") as f:
    resp = httpx.post(
        "https://app.specala.ai/api/v1/developer/transcriptions",
        params={"language": "en"},
        headers={
            "Authorization": "Bearer sk_live_your_key_here",
            "Content-Disposition": 'attachment; filename="standup.mp3"',
        },
        content=f,        # streamed from disk — works for 2 GB files too
        timeout=httpx.Timeout(10, write=None),  # don't time out mid-upload
    )
resp.raise_for_status()
t = resp.json()
print(t["uuid"], t["status"])   # "3fa85f64-…" "queued"
import { openAsBlob } from "node:fs"; // Node 20+

const res = await fetch(
  "https://app.specala.ai/api/v1/developer/transcriptions?language=en",
  {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_live_your_key_here",
      "Content-Disposition": 'attachment; filename="standup.mp3"',
    },
    body: await openAsBlob("standup.mp3"), // streamed, Content-Length set for you
  },
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const t = await res.json();
console.log(t.uuid, t.status); // "3fa85f64-…" "queued"

With the local MCP server connected, just ask:

Upload ~/Recordings/standup.mp3 to Specala and give me the action items.

The assistant calls specala_upload_file, then polls specala_get_transcription until the transcript is ready. Uploads need a key with upload permission and are local-server only — the remote connector can't read files from your disk.

Response

201 Created — the file was received and queued. Transcription happens asynchronously.

{
  "id": 12345,
  "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "queued"
}
FieldTypeDescription
idintegerInternal numeric id.
uuidUUIDUse this with Get transcription to poll and fetch results.
statusstringAlways queued on success.

Status lifecycle & polling

Poll GET /transcriptions/{uuid} every 5–10 seconds until a terminal status:

statusMeaning
queuedAccepted; waiting for a processing slot.
processingTranscription in progress.
completedDone — the response now includes text and prompt_results. Terminal.
failedCouldn't be processed — see error_code below. Terminal.
insufficient_balanceNot enough minutes on your plan. The file is not stored — top up, then upload again. Terminal.
import time

while True:
    t = httpx.get(f"{BASE}/transcriptions/{uuid}", headers=HEADERS).json()
    if t["status"] in ("completed", "failed", "insufficient_balance"):
        break
    time.sleep(10)

Common error_code values on failed:

CodeMeaning
FILE_CORRUPTEDThe file isn't readable media (wrong bytes, damaged container).
FILE_NO_AUDIONo audio stream (e.g. a video with no sound).
RECORDING_TOO_LONGLonger than 10 hours.
UPLOAD_INCOMPLETEFewer bytes arrived than Content-Length promised — upload again.

Limits & billing

LimitValue
File size1 KB – 2 GB, any plan with API access.
Duration1 second – 10 hours.
FormatsAudio: mp3, wav, m4a, aac, ogg, flac, opus, amr, wma, aiff and more. Video: mp4, mov, mkv, avi, webm, mpeg and more — audio is extracted automatically.
ConcurrencyFiles processed in parallel per your plan — extra uploads are not rejected, they wait as queued and start automatically. Simultaneous uploads in flight are capped separately (10 per key, 16 per workspace); over that you get 429, so retry with jitter.
BillingStandard plan minutes, same as uploading in the app. Minutes are reserved when processing starts.

Anti-abuse safeguards (per-key daily caps, concurrent-upload limits) sit far above normal usage — if you hit 429 with upload_quota_exceeded, contact support to raise them.

Retries

Without precautions, retrying an upload whose response you never saw (connection dropped after the last byte) would create a second transcription and a second charge. The Idempotency-Key header solves this:

  • Send a unique value per file (a UUID is perfect) with every attempt.
  • If the file was already accepted under that key, you get 200 with the original uuid instead of a duplicate — no double billing, quotas untouched.
  • A request with the same key still in flight returns 409 idempotency_conflict — wait Retry-After and retry.
  • Add jitter: sleep a random slice of Retry-After rather than the exact value. When many clients are throttled at once, waking them all on the same second just recreates the queue.
  • Only success is remembered (for 24 hours); after a failed attempt the key is free to reuse.
curl -X POST "https://app.specala.ai/api/v1/developer/transcriptions" \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H 'Content-Disposition: attachment; filename="standup.mp3"' \
  -H "Idempotency-Key: 7f3a2b1c-9d4e-4f5a-8b6c-1d2e3f4a5b6c" \
  --data-binary @standup.mp3

Without an Idempotency-Key, retry on errors only

If you don't send the header, never re-POST after a 201 — retry only on network failures, 429 (after the Retry-After pause) and 5xx.

Errors

StatusCodeWhen
400unsupported_file_format / file_too_small / filename_requiredBad extension, < 1 KB, or missing Content-Disposition.
402insufficient_balanceNo minutes left on the plan.
403upload_scope_requiredKey doesn't have upload permission.
408upload_timeoutThe upload stalled (no data for 60 s) or exceeded 90 minutes.
411length_requiredMissing Content-Length.
413file_size_limit_exceededLarger than 2 GB.
429too_many_concurrent_uploads / too_many_queued_files / upload_quota_exceededConcurrency, queue or daily caps — honour Retry-After.

See Errors for the full reference and retry guidance.

On this page