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:
POST /api/v1/subtitles/upload/— hand over the file and the languages.GET /api/v1/subtitles/{id}/— poll until the file reportscompleted.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"]
}
}
}| Status | Code | When |
|---|---|---|
| 401 | AUTHENTICATION_FAILED | The key was rejected by an authenticated endpoint |
| 403 | MISSING_API_KEY / INVALID_API_KEY | No X-Api-Key header, or an unknown or revoked key |
| 403 | PERMISSION_DENIED | The resource belongs to someone else |
| 404 | NOT_FOUND | No such resource |
| 422 | VALIDATION_ERROR | The request body did not validate |
Translate a file
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.
| Field | Required | Description |
|---|---|---|
| file | yes | The subtitle file. .srt, .vtt, .ass, .ssa or .sbv. |
| source_language | yes | Language of the file, in English — e.g. "English (US)". |
| target_language | yes | Language to translate into — e.g. "Turkish". |
| translation_quality | yes | good or best. |
| tier | no | standard (default) or flex — flex is cheaper and slower. |
| instruction | no | Free-text guidance for the translator, up to 2000 characters. |
| is_rtl | no | true if the target language is right-to-left. Defaults to false. |
| prev_lines_count | no | How many preceding lines to give the model as context. Defaults to 3. |
| project_id | no | Attach 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
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
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 parameter | Description |
|---|---|
| project_id | Only files in this project. |
| cursor | The 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
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
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
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.
| Method | Path | Description |
|---|---|---|
| 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
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
| Value | Meaning |
|---|---|
| processing | Being translated. Progress moves as chunks land. |
| completed | Finished. The translation is ready to download. |
| trial_limited | A trial account, so only the first slice was translated. Downloadable. |
| cancelling | A cancel was requested; the worker is winding down. |
| cancelled | Stopped before finishing. |
| failed | Something went wrong. The reason is in info. |
Quality
| Value | Meaning |
|---|---|
| good | The everyday choice. Faster and cheaper. |
| best | A stronger model, for material where nuance matters. |
Tier
| Value | Meaning |
|---|---|
| standard | Normal throughput. The default. |
| flex | Cheaper, 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)