API reference

Translate subtitle files from your own scripts, pipelines and tools.

Overview

Everything the web app does, it does through this API. Translation is asynchronous: you upload a file, the server starts translating it in the background, and you poll for the result. A long film can take several minutes.

The shape of a typical integration is:

  1. POST /api/v1/subtitles/upload/ — hand over the file and the languages.
  2. GET /api/v1/subtitles/{id}/ — poll until the file reports completed.
  3. GET /api/v1/subtitles/{id}/download/ — get a short-lived link to the translated file.
Base URL
https://api.subtranslate.ai
Content type
application/json
Uploads
multipart/form-data
Identifiers
ULIDs — 26-character, sortable, opaque strings
Timestamps
ISO 8601, UTC

Authentication

Every request carries an API key in the X-Api-Key header. Create one on the API Keys page. A key is shown once, when it is created, and only its hash is stored afterwards — if you lose it, revoke it and make another.

curl https://api.subtranslate.ai/api/v1/subtitles/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"

A key acts as your whole account: it can translate files and spend your credits. Keep it out of source control and out of browser code.

A missing or revoked key gets 403:

{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid or expired API key."
  }
}

Responses

A single object comes back under resource; a list comes back under resources. Anything that is about the response rather than the object itself — pagination cursors, your credit balance, a freshly minted key — sits in meta.

{
  "resource": { "id": "01JBS0T1E8S0W8S1234567890", "name": "movie.srt" },
  "meta": { "credits_balance": "12.5000000000" }
}
{
  "resources": [ { "id": "01JBS0T1E8S0W8S1234567890", "name": "movie.srt" } ],
  "meta": { "has_next": true, "cursor": "cD0wMUpCUzBU..." }
}

Errors

Errors always use the same envelope. details is present on validation failures and maps each rejected field to its reasons.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "file": ["Invalid file type. Allowed subtitle formats: .srt, .vtt, .ass, .ssa, .sbv. Got: .txt"]
    }
  }
}
StatusCodeWhen
401AUTHENTICATION_FAILEDThe key was rejected by an authenticated endpoint
403MISSING_API_KEY / INVALID_API_KEYNo X-Api-Key header, or an unknown or revoked key
403PERMISSION_DENIEDThe resource belongs to someone else
404NOT_FOUNDNo such resource
422VALIDATION_ERRORThe request body did not validate

Translate a file

POST/api/v1/subtitles/upload/

Send the subtitle file as multipart/form-data. Translation starts immediately and the response comes back before any of it is done, with the file in processing. Repeat the file part to upload several files in one request; the other fields then apply to all of them.

FieldRequiredDescription
fileyesThe subtitle file. .srt, .vtt, .ass, .ssa or .sbv.
source_languageyesLanguage of the file, in English — e.g. "English (US)".
target_languageyesLanguage to translate into — e.g. "Turkish".
translation_qualityyesgood or best.
tiernostandard (default) or flex — flex is cheaper and slower.
instructionnoFree-text guidance for the translator, up to 2000 characters.
is_rtlnotrue if the target language is right-to-left. Defaults to false.
prev_lines_countnoHow many preceding lines to give the model as context. Defaults to 3.
project_idnoAttach the file to a project, and inherit that project's defaults.
curl -X POST https://api.subtranslate.ai/api/v1/subtitles/upload/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY" \
  -F "file=@movie.srt" \
  -F "source_language=English (US)" \
  -F "target_language=Turkish" \
  -F "translation_quality=good" \
  -F "instruction=Keep character names untranslated"

201 Created

{
  "resources": [
    {
      "id": "01JBS0T1E8S0W8S1234567890",
      "name": "movie.srt",
      "source_language": "English (US)",
      "target_language": "Turkish",
      "translation_quality": "good",
      "tier": "standard",
      "instruction": "Keep character names untranslated",
      "status": "processing",
      "total_lines": 0,
      "last_processed_id": 0,
      "credits": "0.0000000000",
      "created_at": "2026-09-20T12:00:00Z"
    }
  ],
  "meta": {}
}

Check progress

GET/api/v1/subtitles/{id}/

Fetches one file. This is the endpoint to poll while you wait: polling every 10–15 seconds is plenty — a chunk of translation takes longer than that to land.

Progress is last_processed_id out of total_lines. total_lines is 0 until the file has been parsed, so guard against dividing by it. When something goes wrong, the reason is in info.

curl https://api.subtranslate.ai/api/v1/subtitles/01JBS0T1E8S0W8S1234567890/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"
{
  "resource": {
    "id": "01JBS0T1E8S0W8S1234567890",
    "name": "movie.srt",
    "status": "processing",
    "total_lines": 842,
    "last_processed_id": 310,
    "credits": "1.8400000000",
    "info": null,
    "updated_at": "2026-09-20T12:04:11Z"
  },
  "meta": {
    "credits_balance": "12.5000000000"
  }
}

Stop polling on completed or trial_limited, which are both downloadable, and on failed or cancelled, which are not.

List your files

GET/api/v1/subtitles/

Lists your files, newest first, 20 at a time. Use it to enumerate what you have — to follow one translation, poll that file directly instead, so later uploads cannot push it off the first page.

Query parameterDescription
project_idOnly files in this project.
cursorThe value of meta.cursor from the previous page.
curl "https://api.subtranslate.ai/api/v1/subtitles/?project_id=01JBS0T1E8S0W8S1234567890" \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"
{
  "resources": [
    {
      "id": "01JBS0T1E8S0W8S1234567890",
      "name": "movie.srt",
      "status": "processing",
      "total_lines": 842,
      "last_processed_id": 310,
      "credits": "1.8400000000",
      "info": null,
      "updated_at": "2026-09-20T12:04:11Z"
    }
  ],
  "meta": {
    "credits_balance": "12.5000000000",
    "has_next": false,
    "cursor": null
  }
}

Download a translation

GET/api/v1/subtitles/{id}/download/

Returns a presigned link to the translated file, valid for one hour. The link is not a redirect — fetch it yourself. Files in trial_limited are downloadable too: they hold a real translation, just a partial one.

curl https://api.subtranslate.ai/api/v1/subtitles/01JBS0T1E8S0W8S1234567890/download/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"

201 Created

{ "download_url": "https://s3.eu-central-1.amazonaws.com/..." }

Asking before the translation is finished gets 422.

Cancel a translation

POST/api/v1/subtitles/{id}/cancel/

Asks the worker to stop. The file moves to cancelling and then to cancelled once the chunk in flight finishes — so the change is not instant. Work already done is still billed.

curl -X POST https://api.subtranslate.ai/api/v1/subtitles/01JBS0T1E8S0W8S1234567890/cancel/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"

Delete a file

DELETE/api/v1/subtitles/{id}/

Removes the record and both the original and translated files from storage. Returns 204 with no body. A file that is still processing cannot be deleted — cancel it first.

curl -X DELETE https://api.subtranslate.ai/api/v1/subtitles/01JBS0T1E8S0W8S1234567890/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY"

Projects

A project groups files and carries defaults — languages, quality, tier, context size and standing instructions — that an upload inherits when you pass its project_id and leave those fields out.

MethodPathDescription
GET/api/v1/projects/List your projects, 20 at a time.
POST/api/v1/projects/Create a project. Body: { "name": "..." }.
GET/api/v1/projects/{id}/Fetch one project.
PATCH/api/v1/projects/{id}/Update name, defaults or instructions.
DELETE/api/v1/projects/{id}/Delete a project.
curl -X POST https://api.subtranslate.ai/api/v1/projects/ \
  -H "X-Api-Key: $SUBTRANSLATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Season 2"}'

Settable defaults are default_source_language, default_target_language, default_translation_quality, default_tier, default_prev_lines_count and instructions.

Account and credits

GET/api/v1/auth/me/

Returns the account the key belongs to, including credits_balance. Translation spends credits as it goes, so it is worth checking the balance before queueing a batch. Credits are topped up from the billing page, not through the API.

{
  "resource": {
    "id": "01JBS0T1E8S0W8S1234567890",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "is_email_verified": true,
    "credits_balance": "12.5000000000"
  }
}

Field reference

Status

ValueMeaning
processingBeing translated. Progress moves as chunks land.
completedFinished. The translation is ready to download.
trial_limitedA trial account, so only the first slice was translated. Downloadable.
cancellingA cancel was requested; the worker is winding down.
cancelledStopped before finishing.
failedSomething went wrong. The reason is in info.

Quality

ValueMeaning
goodThe everyday choice. Faster and cheaper.
bestA stronger model, for material where nuance matters.

Tier

ValueMeaning
standardNormal throughput. The default.
flexCheaper, at the cost of a longer and less predictable wait.

Formats

.srt, .vtt, .ass, .ssa and .sbv. The translation comes back in the format it went in as.

End-to-end example

Upload a file, wait for it, and save the translation:

import os, time, requests

BASE = "https://api.subtranslate.ai"
HEADERS = {"X-Api-Key": os.environ["SUBTRANSLATE_API_KEY"]}

with open("movie.srt", "rb") as handle:
    created = requests.post(
        f"{BASE}/api/v1/subtitles/upload/",
        headers=HEADERS,
        files={"file": handle},
        data={
            "source_language": "English (US)",
            "target_language": "Turkish",
            "translation_quality": "good",
        },
    )
created.raise_for_status()
file_id = created.json()["resources"][0]["id"]

while True:
    current = requests.get(f"{BASE}/api/v1/subtitles/{file_id}/", headers=HEADERS)
    current.raise_for_status()
    subtitle = current.json()["resource"]

    if subtitle["status"] in ("completed", "trial_limited"):
        break
    if subtitle["status"] in ("failed", "cancelled"):
        raise RuntimeError(subtitle["info"] or subtitle["status"])

    time.sleep(15)

link = requests.get(f"{BASE}/api/v1/subtitles/{file_id}/download/", headers=HEADERS)
link.raise_for_status()

with open("movie.tr.srt", "wb") as out:
    out.write(requests.get(link.json()["download_url"]).content)