Integration API
Every Translatize integration – the CLI, the CI actions and the MCP server – is built on this token-authenticated REST API. It covers labels with their context, exports, git-like branches and merges, AI translation, the glossary and translation memory, QA checks and AI review, screenshot capture and over-the-air releases.
Authentication
Every request authenticates with a Translatize API token – a string beginning with mcni_ – sent as a bearer token in the Authorization header. All endpoints live under https://api.translatize.com/v1/integrations. Your app login session cannot be used here.
curl https://api.translatize.com/v1/integrations/me \
-H "Authorization: Bearer mcni_xxx"A token is bound to exactly one project and one base branch, and carries a role and a branch scope. Create tokens in the app under Project Settings → Integrations. Branch creation, merges and deletions, capture runs and over-the-air releases made with a token show up in the audit log under the token's name.
Roles
A token has one of three roles: translator, developer (the default) or admin. Project members with the developer role or higher can create tokens, never with a role above their own. If the person who created a token leaves the project, or moves to a role below the token's, the token stops working.
Translator tokens read labels, write values and status, run QA checks and start AI translation. Developer tokens can also set label context and tags, create, merge and delete branches, start AI reviews, upload screenshots and publish releases. Merging into main needs an admin token. A role that is too low is rejected with insufficient_permissions. The route overview lists the minimum role of every route.
Branch scope & the branch parameter
A fixed-scope token can only act on its one base branch. A create-own token may additionally create branches (always forked from the base) and read, write, export, compare, merge and delete the branches it created – never any others. Wherever a branch parameter is accepted it defaults to the base branch; naming a branch outside the allowed set returns branch_not_allowed with the token's boundBranch and full allowedBranches list. Merge, rebase and delete additionally require that the branch was created by this token, else not_token_branch.
Response & error envelope
Successful calls return a JSON body (documented per endpoint below). Semantic errors return a machine-readable code – { "error": "branch_not_allowed", ... } – sometimes with extra context fields. Validation failures on the label, branch and translation routes return an errors array of per-field messages; the glossary, memory, QA and screenshot routes answer { "error": "invalid_request" } with the details. See the full error-code table.
Route overview
Paths are relative to https://api.translatize.com/v1. "Any token" means any valid token, whatever its role.
| Method | Path | Minimum role |
|---|---|---|
| GET | /integrations/me | any token |
| GET | /integrations/projects/{projectId}/labels | translator |
| GET | /integrations/projects/{projectId}/export | translator |
| PATCH | /integrations/projects/{projectId}/labels | translator; developer to send context or tags |
| PATCH | /integrations/projects/{projectId}/labels/batch | translator; developer to send context or tags |
| GET | /integrations/projects/{projectId}/branches | any token |
| POST | /integrations/projects/{projectId}/branches | developer, create-own scope |
| GET | /integrations/projects/{projectId}/branches/{branchName}/compare | any token |
| GET | /integrations/projects/{projectId}/branches/{branchName}/conflicts | any token |
| POST | /integrations/projects/{projectId}/branches/{branchName}/merge | developer, own branch; admin into main |
| POST | /integrations/projects/{projectId}/branches/{branchName}/labels/rebase | developer, own branch |
| DELETE | /integrations/projects/{projectId}/branches/{branchName} | developer, own branch |
| POST | /integrations/projects/{projectId}/translation/auto-translate | any token; Professional or Agency plan |
| GET | /integrations/projects/{projectId}/translation/status | any token |
| GET | /integrations/projects/{projectId}/glossary | translator |
| POST | /integrations/projects/{projectId}/tm/matches | translator |
| POST | /integrations/projects/{projectId}/qa/run | translator |
| GET | /integrations/projects/{projectId}/qa/issues | translator |
| GET | /integrations/projects/{projectId}/branches/{branchName}/merge-gate | translator |
| POST | /integrations/projects/{projectId}/qa/ai-review | developer; Professional or Agency plan |
| GET | /integrations/projects/{projectId}/qa/ai-review/status | translator |
| POST | /integrations/projects/{projectId}/screenshots/runs | developer; Starter plan and above |
| POST | /integrations/projects/{projectId}/screenshots/runs/{runId}/check | developer |
| POST | /integrations/projects/{projectId}/screenshots/runs/{runId}/screens | developer; Starter plan and above |
| POST | /integrations/projects/{projectId}/screenshots/runs/{runId}/complete | developer |
| GET | /integrations/projects/{projectId}/ota/distributions | translator |
| POST | /integrations/projects/{projectId}/ota/distributions/{distributionId}/releases | developer |
Token introspection
/integrations/meDescribe the calling token: its project, the project's source language, the base branch, branch scope, role and non-secret settings. Any valid token may introspect itself. Secrets (the token itself, its IP allowlist, webhook ids) are never returned.
{
"project": {
"id": "6659f0c3a1b2c3d4e5f60718",
"name": "Acme Web",
"langs": ["en", "lv", "ru"]
},
"branch": "main",
"branchScope": "fixed",
"sourceLanguage": "en",
"role": "developer",
"token": {
"name": "ci-token",
"autoPublish": false,
"expiresAt": null
}
}Labels
Reading labels and writing their values and status needs the translator role. Setting context or tags needs the developer role: a translator token that sends either gets insufficient_permissions for the whole request, batch included. When a token has auto-publish switched on, its label writes also fire the project's webhooks.
Label context and "do not translate"
Context tells translators and AI translation what a string is. Send it as a context object on a label write. Only the fields you send change; the others stay as they are.
| Field | Type | Meaning |
|---|---|---|
| description | string | Which UI element this is and where it appears, up to 1,000 characters. An empty string clears it. |
| charLimit | integer | null | Maximum length of every translation, 1 to 10,000. Values longer than the limit are refused with character_limit_exceeded. null removes the limit. |
| location | string | Where the string is used, for example src/checkout/Pay.tsx:42, up to 300 characters. |
| doNotTranslate | boolean | true for text that is the same in every language – product names, brands, codes. AI translation copies the source text into the other languages instead of translating it, unbilled; quality checks skip the label and translation memory does not learn from it. false clears it. |
For example, to create a product name that stays the same in every language (a developer token; set PROJECT_ID and TRANSLATIZE_API_TOKEN first):
curl -X PATCH https://api.translatize.com/v1/integrations/projects/$PROJECT_ID/labels \
-H "Authorization: Bearer $TRANSLATIZE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":"brand.name","values":{"en":"Translatize"},"context":{"description":"Product name in the page header","doNotTranslate":true}}'/integrations/projects/{projectId}/labelsList labels on a branch, sorted by key ascending. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branch | string | query | Branch to read. Defaults to the token's base branch. |
| namespace | string | query | Keep only keys prefixed <namespace>. (a dotted prefix). |
| status | string | query | One of draft, review, approved, rejected. |
{
"branch": "main",
"total": 2,
"labels": [
{
"key": "app.title",
"values": { "en": "My App" },
"status": "draft",
"tags": [],
"updatedAt": "2026-07-19T09:12:04.771Z"
},
{
"key": "greeting.hello",
"values": { "en": "Hello", "lv": "Sveiki" },
"status": "approved",
"tags": ["ui"],
"updatedAt": "2026-07-19T09:12:05.133Z"
}
]
}/integrations/projects/{projectId}/exportDownload labels as a translation file. The JSON format is a flat, language-keyed object; every configured language is a top-level key. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| format | string | query | json (default), yaml, csv, xliff, po, arb, resx, ios, or android. json, yaml and csv carry every language in one file; the rest are single-language and default to the project's first language when lang is omitted. |
| lang | string | query | Restrict the export to a single language. |
| sourceLang | string | query | Language to translate from, for the bilingual formats (xliff, and the translator comments in po). Ignored by every other format. Defaults to English when the project has it, otherwise another project language. |
| namespace | string | query | Dotted key-prefix filter. |
| branch | string | query | Branch to export. Defaults to the base branch. |
The response is served as a file download (for JSON, Content-Type: application/json with a Content-Disposition attachment). A key appears under a language only when it has a value there.
{
"en": {
"app.title": "My App",
"greeting.hello": "Hello"
},
"lv": {
"greeting.hello": "Sveiki"
}
}/integrations/projects/{projectId}/labelsCreate or update a single label (upsert by key) on a branch. Translator role or higher; context and tags need the developer role.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| key* | string | body | Dotted label key, 1–1000 chars, no empty segments (and no __proto__/constructor/prototype segment). |
| values | object | body | { langCode: string }. Every language must be configured on the project, else unknown_languages. An empty string empties that language. |
| status | string | body | One of draft, review, approved, rejected. |
| tags | string[] | body | Replaces the label's tags. At most 20, each up to 50 characters. Developer role. |
| context | object | body | { description?, charLimit?, location?, doNotTranslate? } – see label context. Developer role. |
| branch | string | body | Branch to write to. Defaults to the base branch. |
At least one of values, status, tags or context is required. created is true when the key did not exist on the branch before. context is returned when the request sent one.
A request that only sets context or tags for a key that does not exist fails with label_not_found and creates nothing. A value over the label's character limit fails with character_limit_exceeded (a charLimit sent in the same request applies to its values). A new key beyond your plan's source-key limit fails with source_key_limit_reached.
{
"key": "brand.name",
"branch": "main",
"created": true,
"values": { "en": "Translatize" },
"status": "draft",
"tags": [],
"context": {
"description": "Product name in the page header",
"doNotTranslate": true
}
}/integrations/projects/{projectId}/labels/batchUpsert up to 500 labels in one call. Each item is upserted on its own – a rejected item lands in failed[] instead of aborting the batch. Translator role or higher; any item with context or tags needs the developer role.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| labels* | object[] | body | Array of { key, values?, status?, tags?, context? }, each with the same rules as the single upsert. Max 500 items, else batch_too_large. |
| branch | string | body | Branch to write to. Defaults to the base branch. |
Failures are collected per key: unknown_languages, character_limit_exceeded, label_not_found (a context- or tags-only item for a missing key), or invalid_input for an item that is malformed. The rest of the batch still applies. If the new keys in the batch would go over your plan's source-key limit, the whole request is refused up front with source_key_limit_reached (carrying limit, current and attempted).
{
"updated": 1,
"created": 2,
"failed": [
{ "key": "checkout.old_title", "error": "label_not_found" }
]
}Branches
Branch reads work with any token. Creating a branch needs a create-own token with the developer role, and merge, rebase and delete work only on branches the token created. Merges are non-destructive by default and never delete the source branch.
/integrations/projects/{projectId}/branchesList every branch in the project, each flagged with this token's access. main is always listed. Any valid token.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
{
"baseBranch": "main",
"branchScope": "create-own",
"branches": [
{
"name": "main",
"description": "Main production branch",
"basedOn": null,
"createdAt": "2026-07-10T08:00:00.000Z",
"createdByThisToken": false,
"writable": true
},
{
"name": "feat-checkout",
"description": "checkout copy",
"basedOn": "main",
"createdAt": "2026-07-19T09:20:11.402Z",
"createdByThisToken": true,
"writable": true
}
]
}/integrations/projects/{projectId}/branchesCreate a branch forked from the token's base branch. Needs a create-own token and the developer role; a fixed-scope token is rejected with branch_scope_fixed.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| name* | string | body | 1–100 chars matching ^[a-zA-Z0-9_-]+$, and not main (invalid_name). An existing name fails with branch_exists. |
| description | string | body | Optional, max 500 chars. |
| basedOn | string | body | Optional and forced to the base branch – any other value returns branch_not_allowed. |
Creating a branch copies the base branch's labels into it. The plan's branch limit is enforced (branch_limit_reached, with limit and current).
{
"branch": {
"name": "feat-checkout",
"description": "checkout copy",
"isDefault": false,
"basedOn": "main",
"createdBy": "6650aa11bb22cc33dd44ee55",
"createdAt": "2026-07-19T09:20:11.402Z",
"lastModified": "2026-07-19T09:20:11.402Z",
"createdByName": "Alex Rivera"
}
}/integrations/projects/{projectId}/branches/{branchName}/compareDiff a branch (source) against the token's base branch (target): changed, added and deleted keys, each with the differing per-language source/target values. Any valid token; the branch must be in the allowed set.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
{
"source": "feat-checkout",
"target": "main",
"differences": {
"changed": [],
"added": [
{
"key": "checkout.pay_button",
"languages": {
"en": { "sourceValue": "Pay now", "targetValue": null }
}
}
],
"deleted": []
},
"summary": { "changed": 0, "added": 1, "deleted": 0, "total": 1 }
}/integrations/projects/{projectId}/branches/{branchName}/conflictsFlat per-key/per-language conflicts of a branch (source) against the base (target) – cases where both sides hold a different non-empty value. An empty conflicts array means the branch is safe to merge with the default strategy. Any valid token.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
{
"source": "feat-checkout",
"target": "main",
"conflicts": [
{
"key": "app.title",
"lang": "lv",
"sourceValue": "Veikals",
"targetValue": "Sākums"
}
],
"hasConflicts": true,
"conflictCount": 1
}/integrations/projects/{projectId}/branches/{branchName}/mergeMerge a branch this token created into the token's base branch. Developer role, and the admin role when the base is main. The target is always the base, and the source branch is kept.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
| strategy | string | body | overwrite (default) – a non-destructive union: the branch wins every conflict and keys only on the base stay. keep-newer – in a conflict, the label changed more recently wins (the branch only when it is strictly newer). manual – you resolve each conflict with conflicts; unresolved ones keep the base value. branch-changes – only the values, context fields and tags the branch itself changed since it was created are written, the branch's change winning; everything else on the base stays as it is and nothing is deleted. It is the same merge the GitHub App makes when a pull request is merged. replace – destructive: the base becomes an exact copy of the branch. |
| conflicts | object[] | body | Per-conflict resolutions for the manual strategy: { key, lang, resolution: "source"|"target"|"custom", customValue? }. customValue is required with custom. |
| target | string | body | Optional and forced to the base branch – any other value returns branch_not_allowed. |
curl -X POST https://api.translatize.com/v1/integrations/projects/$PROJECT_ID/branches/feat-checkout/merge \
-H "Authorization: Bearer $TRANSLATIZE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"strategy":"branch-changes"}'The checks run in this order. The branch must be one this token created (not_token_branch). Merging into main needs an admin token: any other role gets admin_token_required_for_main – create an admin token for that job, or merge in the app. Then the project's QA merge gate checks what the merge would write. A blocking gate answers qa_gate_blocked with the gate and its issues. A token can never override it: fix the errors and merge again, or an admin or owner merges in the app with a written reason.
If the gate can't be checked while the project's mode is block, nothing is merged and you get qa_gate_unavailable (HTTP 503); in warn mode the merge goes ahead and the response's gate carries "error": "gate_unavailable". If someone writes to the branch between the gate check and the merge, nothing is merged and you get branch_changed_retry (HTTP 409) – run the merge again. overlappingKeys and overlappingCount come only with branch-changes: merged keys the base had also changed in the meantime, worth a second look.
{
"message": "Branch merged successfully",
"source": "feat-checkout",
"target": "main",
"strategy": "branch-changes",
"gate": {
"mode": "warn",
"applies": true,
"blocked": false,
"errors": 0,
"warnings": 2,
"pairsChecked": 36,
"overridden": false
},
"overlappingKeys": ["checkout.title"],
"overlappingCount": 1
}/integrations/projects/{projectId}/branches/{branchName}/labels/rebaseMark the current values of some keys as the branch's new starting point, without changing them. Use it after you set a key on your branch back to the base's text, so a later branch-changes merge does not carry that key over a newer change on the base. Developer role; only branches this token created.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
| keys* | string[] | body | 1 to 500 label keys. |
| langs | string[] | body | Up to 100 language codes. Omit it to cover every language the label holds; languages the project does not have are ignored. |
rebased counts the labels updated. Keys that don't exist on the branch are listed in notFound instead of failing the request.
{
"rebased": 2,
"notFound": ["checkout.old_title"]
}/integrations/projects/{projectId}/branches/{branchName}Delete a branch this token created, with its labels – cleanup after a merge. Developer role. The base branch, main and branches created by anyone else cannot be deleted (not_token_branch).
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
Other tokens bound to the deleted branch stop working with token_inactive until someone points them at an existing branch in the app.
{
"message": "Branch deleted successfully"
}AI translation
Platform AI translation is part of the Professional and Agency plans. On other plans the launch returns feature_not_available – export the missing values and translate them yourself instead. Before the AI runs, exact matches from the project's translation memory fill what they can (unless the project switched that off), and "do not translate" labels get the source text copied. Neither is billed. The AI also receives the label's context, the glossary and the project's style guide – see AI translation.
/integrations/projects/{projectId}/translation/auto-translateStart a background AI job that fills in missing translations on a branch. Returns HTTP 202 with the started job, or HTTP 200 with a no-op when nothing needs work. Any valid token, translator role included, on a plan with AI translation.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| targetLangs* | string[] | body | Languages to translate into (at least one). Each must be configured on the project, else unknown_target_language. |
| sourceLang | string | body | Language to translate from. Defaults to the project's source language (sourceLanguage in /me): the one set in Project Settings, else English when the project has it, else its first language. A language the project doesn't have fails with unknown_source_language. |
| labelKeys | string[] | body | Restrict to these keys; omit to translate every label needing work. |
| overwriteTranslated | boolean | body | Re-translate values that already have a translation. Defaults to false. |
| branch | string | body | Branch to translate. Defaults to the base branch. |
When nothing is eligible, the response is HTTP 200 with { "started": false, "nothingToTranslate": true, "reason": "already_translated", ... } (or no_source when the labels have no source text). An exhausted monthly allowance returns ai_quota_exceeded (with used, limit, remaining and estimated), and a project that already has a job running returns translation_already_running. Follow progress with the status endpoint below.
{
"started": true,
"branch": "main",
"job": {
"id": "6659f1a2b3c4d5e6f7081920",
"status": "queued",
"targetLangs": ["lv", "ru"],
"counts": {
"requestedLabels": 12,
"translatedValues": 0,
"failedValues": 0,
"skippedValues": 0,
"fromMemory": 0,
"keptValues": 0
},
"chars": { "estimated": 840, "actual": 0 }
}
}/integrations/projects/{projectId}/translation/statusThe project's live translation job (or null), its most recent job, and the AI allowance of the subscription the project bills to. Any valid token.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
fromMemory counts values filled from translation memory and keptValues the source text copied into "do not translate" labels; neither is part of translatedValues or billed. While a job waits to start, activeJob carries its position in line; once running, its requestedLabels and startedAt.
{
"activeJob": null,
"lastJob": {
"id": "6659f1a2b3c4d5e6f7081920",
"status": "completed",
"branch": "main",
"sourceLang": "en",
"targetLangs": ["lv", "ru"],
"counts": {
"requestedLabels": 12,
"translatedValues": 21,
"failedValues": 0,
"skippedValues": 0,
"fromMemory": 3,
"keptValues": 0
},
"chars": { "estimated": 840, "actual": 734 },
"error": null,
"startedAt": "2026-07-19T09:25:00.000Z",
"finishedAt": "2026-07-19T09:25:48.311Z"
},
"aiQuota": { "used": 734, "limit": 2000000, "remaining": 1999266 }
}Glossary & translation memory
Both are project-wide, so neither route takes a branch. Agents use them to keep wording consistent when they translate on their own.
/integrations/projects/{projectId}/glossaryThe project's glossary terms plus those shared in its workspace: required translations per language, case sensitivity and prohibited terms. A project term wins over a workspace term with the same text. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
{
"terms": [
{
"id": "6659f0c3a1b2c3d4e5f60801",
"term": "workspace",
"translations": { "lv": "darbvieta" },
"description": "A team's shared area",
"caseSensitive": false,
"prohibited": false,
"category": "product",
"scope": "project"
}
]
}/integrations/projects/{projectId}/tm/matchesLook up translation-memory matches for source texts, with placeholders mapped onto your text. Matches come from the project's own memory and, while the project shares its memory with its workspace, from other projects in the workspace that share theirs. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| texts* | string[] | body | 1 to 100 source texts, each up to 10,000 characters and 100,000 in total. |
| sourceLang* | string | body | Language of the texts. |
| targetLang* | string | body | Language you want matches in. |
| threshold | number | body | Minimum match percentage, 50 to 100. Default 75. |
At most 3 matches per text; a text without a match returns an empty matches array. origin says where the entry came from: approved (an approved label), human_edit (a person edited the value) or manual (added or edited in the memory itself). quality runs from 0 to 1.
{
"results": [
{
"text": "Save changes",
"matches": [
{
"sourceText": "Save changes",
"targetText": "Saglabāt izmaiņas",
"match": 100,
"quality": 1,
"placeholderMapped": false,
"origin": "approved"
}
]
}
]
}QA checks, the merge gate & AI review
Ten rule checks run whenever labels change: placeholders, HTML tags, ICU syntax, ICU plural categories, glossary terms, length (the character limit), numbers, punctuation, untranslated and whitespace. Counts always leave out ignored issues. The merge gate summarises what a merge would introduce; its mode (off, warn – the default – or block) and whether it covers merges into main or into every branch are project settings. Tokens can never override a blocking gate. More in Quality checks.
/integrations/projects/{projectId}/qa/runRun the checks on a branch now. Translator role or higher. Changes no translation.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branch | string | body | Defaults to the token's base branch. |
| keys | string[] | body | Limit the run to these keys (up to 10,000). |
The inline response lists at most 200 open issues, errors first. A large branch, or one that is already being checked, continues in the background: HTTP 202 with { "branch": "feat-checkout", "background": true }. Read the result with the issues endpoint.
{
"branch": "feat-checkout",
"pairsChecked": 84,
"counts": { "error": 1, "warning": 3, "info": 0 },
"issues": [
{
"id": "6659f1a2b3c4d5e6f7081931",
"branch": "feat-checkout",
"key": "checkout.total",
"lang": "lv",
"checkId": "placeholders",
"severity": "error",
"code": "placeholder_missing",
"params": { "placeholders": ["{amount}"], "list": "{amount}", "count": 1 },
"source": "rule",
"ignored": false,
"createdAt": "2026-11-03T09:12:04.771Z"
}
]
}/integrations/projects/{projectId}/qa/issuesStored issues of a branch, paged by cursor. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branch | string | query | Defaults to the token's base branch. |
| severity | string | query | error, warning or info. |
| key | string | query | Only issues of this key. |
| lang | string | query | Only issues of this language. |
| source | string | query | rule for the automatic checks, ai for AI review findings. |
| includeIgnored | boolean | query | Also list issues someone chose to ignore (with ignoredReason and ignoredAt). |
| cursor | string | query | The nextCursor of the previous page. |
| limit | number | query | Page size, 1 to 500. Default 100. |
counts are the open issues of the branch, narrowed by key, lang and source but not by severity. Issues have the same shape as in the run response; AI review findings may add message and suggestion.
{
"issues": [],
"nextCursor": null,
"counts": { "error": 1, "warning": 3, "info": 0 }
}/integrations/projects/{projectId}/branches/{branchName}/merge-gatePreview the merge gate for merging a branch into the token's base branch, the only target a token merges into. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branchName* | string | path | A branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$. |
| strategy | string | query | The strategy you will merge with (default overwrite). The gate checks different pairs per strategy – branch-changes only what changed on the branch. |
applies is false when the mode is off, or when the gate covers only merges into main and the base is another branch. A branch that doesn't exist answers source_branch_not_found.
{
"source": "feat-checkout",
"target": "main",
"gate": {
"mode": "block",
"applies": true,
"blocked": true,
"errors": 1,
"warnings": 3,
"pairsChecked": 36,
"issues": []
}
}/integrations/projects/{projectId}/qa/ai-reviewQueue an AI review of translations on a branch. Professional and Agency plans, billed against the separate AI review allowance. Developer role. Findings are warnings at most and never block a merge.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| branch | string | body | Defaults to the token's base branch. |
| keys | string[] | body | Limit the review to these keys (up to 10,000). |
| langs | string[] | body | Limit the review to these target languages (up to 100). |
A plan without AI review returns feature_not_available with "feature": "aiReview". Other errors: unknown_languages (a language the project can't review), nothing_to_review, too_many_pairs (narrow it with keys or langs), ai_review_quota_exceeded and ai_review_in_progress (one review per project at a time). The findings appear in the issues endpoint with source=ai.
{
"jobId": "6659f1a2b3c4d5e6f7081930",
"status": "queued",
"pairs": 120,
"estimatedCharacters": 9400
}/integrations/projects/{projectId}/qa/ai-review/statusThe latest AI review job on a branch this token may use (or null), with progress counts, the allowance and the project's source language. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
trigger is auto for a review started by the project's "AI review after AI translation" setting. An empty langs means every target language.
{
"job": {
"id": "6659f1a2b3c4d5e6f7081930",
"status": "completed",
"branch": "feat-checkout",
"trigger": "manual",
"langs": [],
"counts": { "total": 120, "reviewed": 120, "failed": 0, "issues": 7 },
"estimatedCharacters": 9400,
"charactersBilled": 9312,
"error": null,
"createdAt": "2026-11-03T09:30:00.000Z",
"startedAt": "2026-11-03T09:30:01.000Z",
"finishedAt": "2026-11-03T09:31:12.000Z"
},
"quota": { "enabled": true, "used": 9312, "limit": 1000000, "remaining": 990688, "month": "2026-11" },
"sourceLanguage": "en"
}Screenshot capture runs
A capture run is a batch of screenshots with text positions, normally uploaded by translatize screenshots upload from @translatize/playwright captures. Automated capture is part of the Starter plan and above; on Free, creating a run or uploading a screen returns feature_not_available with "feature": "screenshotCapture". Every route needs the developer role. Screenshots are visible only to project members, count against the plan's screenshot storage and are never sent to AI. See Screenshots.
/integrations/projects/{projectId}/screenshots/runsStart a capture run. Returns HTTP 201. A project can start 30 runs in a rolling 24 hours; the next one gets capture_run_limit (HTTP 429, with Retry-After).
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| name* | string | body | A label for the run, for example the CI job. Cut to 200 characters. |
| branch | string | body | The branch whose keys the screens link to. Defaults to the token's base branch. |
| commitSha | string | body | The commit the captures were taken from (7 to 64 hex characters). |
{ "runId": "6659f1a2b3c4d5e6f7081940" }/integrations/projects/{projectId}/screenshots/runs/{runId}/checkAsk which images the server already holds, so unchanged screens are not uploaded again.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| runId* | string | path | The capture run, from the create call. It must be on a branch the token may use. |
| clientHashes* | string[] | body | Up to 500 image fingerprints: the SHA-256 of each image file, as 64 lowercase hex characters. |
A run that is already completed answers run_not_open (HTTP 409).
{ "existing": ["9f2c4e…"] }/integrations/projects/{projectId}/screenshots/runs/{runId}/screensUpload one screen as multipart/form-data. Returns HTTP 201. A run holds at most 500 screens (run_screen_limit).
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| runId* | string | path | The capture run, from the create call. It must be on a branch the token may use. |
| meta* | string | body | JSON, up to 1 MB: { name, viewport?: { width, height }, locale?, clientHash?, regions[] }. Each region is { text?, key?, rect: { x, y, w, h } }, in viewport pixels when viewport is given, else in image pixels. A region with a key links that key; one with only text is matched to a key by its text in that locale. |
| image | file | body | PNG, JPEG or WebP, up to 10 MB. Leave it out when clientHash is one the check call reported as existing; otherwise the call fails with image_required. |
links is the number of keys the screen was linked to. Errors: invalid_meta, meta_too_large, invalid_image, image_too_large, screenshot_quota_exceeded (storage full), run_screen_limit and run_not_open. A 503 with Retry-After means the server is busy; send that screen again after the wait.
{ "screenshotId": "6659f1a2b3c4d5e6f7081950", "deduped": false, "links": 12 }/integrations/projects/{projectId}/screenshots/runs/{runId}/completeFinish the run. Newer captures replace older ones of the same screen name, viewport and locale. Calling it again on a completed run returns the same result.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| runId* | string | path | The capture run, from the create call. It must be on a branch the token may use. |
linked counts key links on the run's screens, ambiguous the links worth a check in the app, and superseded the older captures this run replaced.
{
"run": {
"id": "6659f1a2b3c4d5e6f7081940",
"name": "e2e main",
"branch": "main",
"commitSha": "4f1c2ab",
"status": "completed",
"counts": { "screens": 18, "linked": 16, "ambiguous": 1, "superseded": 3 },
"startedBy": { "type": "token", "name": "ci-token" },
"createdAt": "2026-11-03T09:40:00.000Z",
"completedAt": "2026-11-03T09:41:30.000Z"
}
}Over-the-air releases
A distribution publishes one branch of a project to our CDN as versioned JSON files that apps fetch with @translatize/ota. Delivery is an add-on on the Starter plan and above. Anything you publish is public – never put secrets in translations. See Delivery (CDN).
/integrations/projects/{projectId}/ota/distributionsThe project's distributions on branches this token may use. Translator role or higher.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
publishFilter is all or approved, format is json-nested or json-flat, and latestVersion is the version the manifest serves now (after a rollback, the version you rolled back to).
{
"distributions": [
{
"id": "6659f1a2b3c4d5e6f7081960",
"name": "Web app",
"publicId": "k3J9xQ2mP0aZ7vB1cD5eFg",
"branch": "main",
"publishFilter": "all",
"format": "json-nested",
"autoPublishOnMerge": false,
"status": "active",
"latestVersion": 4,
"manifestUrl": "https://cdn.translatize.com/d/k3J9xQ2mP0aZ7vB1cD5eFg/manifest.json"
}
]
}/integrations/projects/{projectId}/ota/distributions/{distributionId}/releasesPublish the distribution's branch as a new release. Returns HTTP 201. Developer role, and the distribution's branch must be in the token's allowed set (branch_not_allowed). Send an empty JSON body – the distribution decides branch, format and filter.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
| distributionId* | string | path | The distribution to publish (distribution_not_found if it does not exist). |
Publishing works whether or not the account holds delivery blocks; the CDN serves a distribution only while it does. A branch with nothing to publish (no values, or no approved labels with the approved filter) answers nothing_to_publish (HTTP 422).
{
"release": {
"version": 5,
"labelCount": 312,
"languages": {
"en": { "path": "/d/k3J9xQ2mP0aZ7vB1cD5eFg/v5/en.json", "hash": "…", "bytes": 18211 }
},
"createdAt": "2026-11-02T09:12:04.771Z"
},
"manifestUrl": "https://cdn.translatize.com/d/k3J9xQ2mP0aZ7vB1cD5eFg/manifest.json"
}Error codes
Semantic errors carry a machine-readable error code; some add context fields (shown in the notes). Validation failures return an errors array or invalid_request, depending on the route.
| Code | HTTP | When it happens |
|---|---|---|
| access_denied | 401 | Missing or malformed Authorization header, an app login token instead of an mcni_ token, or an unknown or revoked token. |
| access_denied | 403 | The token's creator has left the project or now has a role below the token's. |
| token_expired | 401 | The token's expiresAt has passed. |
| token_inactive | 403 | The token's bound branch was deleted; point it at an existing branch to reactivate it (carries reason). |
| ip_not_allowed | 403 | The request IP is not in the token's IP allowlist. |
| project_mismatch | 403 | The {projectId} in the path is not the token's project. |
| project_not_found | 404 | The token's project has been removed. |
| insufficient_permissions | 403 | The token's role is below the route's minimum, or a translator token sent context or tags. |
| branch_not_allowed | 400 | The named branch is not in the token's allowed set (carries boundBranch and allowedBranches). |
| branch_not_found | 404 / 409 | The branch does not exist (409 when a distribution's branch was deleted). |
| branch_scope_fixed | 403 | A fixed-scope token tried to create a branch. |
| not_token_branch | 403 | Merge, rebase or delete targeted a branch this token did not create. |
| invalid_name | 400 | Attempted to create a branch named "main". Other malformed names fail validation and come back in the errors array. |
| branch_exists | 400 | A branch with that name already exists. |
| branch_limit_reached | 403 | The project hit its plan's branch limit (carries limit and current). |
| source_branch_not_found | 404 | The merge-gate preview named a branch that does not exist. |
| strategy_not_supported_for_branch | 400 | branch-changes was asked for with main as the source. |
| admin_token_required_for_main | 403 | A token without the admin role tried to merge into main. |
| qa_gate_blocked | 409 | The QA merge gate blocks the merge (carries gate). A token cannot override it. |
| qa_gate_unavailable | 503 | The gate could not be checked while the mode is block; nothing was merged. |
| branch_changed_retry | 409 | The branch changed between the gate check and the merge; nothing was merged. Retry. |
| unknown_languages | 400 | A label value used a language not configured on the project, or an AI review named one it cannot review (carries languages). |
| character_limit_exceeded | 400 | A value is longer than the label's character limit. |
| label_not_found | 404 | A context- or tags-only write named a key that does not exist. |
| source_key_limit_reached | 403 | New keys would exceed the plan's source-key limit (carries limit and current). |
| batch_too_large | 400 | More than 500 labels in one batch request (carries max: 500). |
| feature_not_available | 403 | The plan does not include the feature – AI translation by default, otherwise named in feature (aiReview, screenshotCapture). |
| unknown_source_language | 400 | The sourceLang of an AI job is not a project language (carries language). |
| unknown_target_language | 400 | A targetLangs entry is not configured on the project (carries languages). |
| too_many_labels | 400 | The job would span more than 100,000 labels (carries limit). |
| ai_quota_exceeded | 403 | The AI character allowance is exhausted (carries used, limit, remaining, estimated). |
| translation_already_running | 409 | A translation job is already running for this project. |
| nothing_to_review | 400 | There is nothing to review in that branch, key and language scope. |
| too_many_pairs | 400 | The AI review scope is too large; narrow it with keys or langs (carries limit and pairs). |
| ai_review_quota_exceeded | 403 | The AI review allowance is exhausted (carries used, limit, remaining, estimated). |
| ai_review_in_progress | 409 | An AI review is already running for this project. |
| capture_run_limit | 429 | More than 30 capture runs in 24 hours (carries limit and Retry-After). |
| run_not_found | 404 | The capture run does not exist in this project. |
| run_not_open | 409 | The capture run is already completed or closed. |
| run_screen_limit | 429 | More than 500 screens in one capture run (carries limit). |
| invalid_meta | 400 | The meta field of a screen upload is missing or not valid JSON. |
| meta_too_large | 413 | The meta field is over 1 MB. |
| image_required | 400 | The screen has no image and the server does not hold one with that clientHash. |
| invalid_image | 400 | The file is not a readable PNG, JPEG or WebP image. |
| image_too_large | 413 | The image is over 10 MB or too large in pixels. |
| screenshot_quota_exceeded | 403 | The account's screenshot storage is full (carries used and limit). |
| distribution_not_found | 404 | The distribution does not exist in this project. |
| nothing_to_publish | 422 | The distribution has nothing to publish (carries publishFilter). |
| invalid_request | 400 | Validation failed on a glossary, memory, QA or screenshot route (with detail or details). |
| errors | 400 | Validation failed on a label, branch or translation route – an array of per-field messages. |
| rate_limit_exceeded | 429 | More than 2,000 requests from one IP address in 15 minutes. |
Limits
Batch upserts take at most 500 labels per request, and a label at most 20 tags. A single AI job may span at most 100,000 labels. A memory lookup takes at most 100 texts. A capture run holds up to 500 screens, and a project can start 30 runs in 24 hours. AI translation and AI review are metered in characters against the monthly allowance of the subscription the project bills to. The whole integration API is rate-limited to 2,000 requests per IP address per 15 minutes; going over returns HTTP 429 with { "error": "rate_limit_exceeded" }.
mcni_ tokens.