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.

bash
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.

MethodPathMinimum role
GET/integrations/meany token
GET/integrations/projects/{projectId}/labelstranslator
GET/integrations/projects/{projectId}/exporttranslator
PATCH/integrations/projects/{projectId}/labelstranslator; developer to send context or tags
PATCH/integrations/projects/{projectId}/labels/batchtranslator; developer to send context or tags
GET/integrations/projects/{projectId}/branchesany token
POST/integrations/projects/{projectId}/branchesdeveloper, create-own scope
GET/integrations/projects/{projectId}/branches/{branchName}/compareany token
GET/integrations/projects/{projectId}/branches/{branchName}/conflictsany token
POST/integrations/projects/{projectId}/branches/{branchName}/mergedeveloper, own branch; admin into main
POST/integrations/projects/{projectId}/branches/{branchName}/labels/rebasedeveloper, own branch
DELETE/integrations/projects/{projectId}/branches/{branchName}developer, own branch
POST/integrations/projects/{projectId}/translation/auto-translateany token; Professional or Agency plan
GET/integrations/projects/{projectId}/translation/statusany token
GET/integrations/projects/{projectId}/glossarytranslator
POST/integrations/projects/{projectId}/tm/matchestranslator
POST/integrations/projects/{projectId}/qa/runtranslator
GET/integrations/projects/{projectId}/qa/issuestranslator
GET/integrations/projects/{projectId}/branches/{branchName}/merge-gatetranslator
POST/integrations/projects/{projectId}/qa/ai-reviewdeveloper; Professional or Agency plan
GET/integrations/projects/{projectId}/qa/ai-review/statustranslator
POST/integrations/projects/{projectId}/screenshots/runsdeveloper; Starter plan and above
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/checkdeveloper
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/screensdeveloper; Starter plan and above
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/completedeveloper
GET/integrations/projects/{projectId}/ota/distributionstranslator
POST/integrations/projects/{projectId}/ota/distributions/{distributionId}/releasesdeveloper

Token introspection

GET/integrations/me

Describe 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.

Example response
{
  "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.

FieldTypeMeaning
descriptionstringWhich UI element this is and where it appears, up to 1,000 characters. An empty string clears it.
charLimitinteger | nullMaximum length of every translation, 1 to 10,000. Values longer than the limit are refused with character_limit_exceeded. null removes the limit.
locationstringWhere the string is used, for example src/checkout/Pay.tsx:42, up to 300 characters.
doNotTranslatebooleantrue 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):

bash
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}}'
GET/integrations/projects/{projectId}/labels

List labels on a branch, sorted by key ascending. Translator role or higher.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchstringqueryBranch to read. Defaults to the token's base branch.
namespacestringqueryKeep only keys prefixed <namespace>. (a dotted prefix).
statusstringqueryOne of draft, review, approved, rejected.
Example response
{
  "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"
    }
  ]
}
GET/integrations/projects/{projectId}/export

Download 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
formatstringqueryjson (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.
langstringqueryRestrict the export to a single language.
sourceLangstringqueryLanguage 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.
namespacestringqueryDotted key-prefix filter.
branchstringqueryBranch 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.

Example response (format=json)
{
  "en": {
    "app.title": "My App",
    "greeting.hello": "Hello"
  },
  "lv": {
    "greeting.hello": "Sveiki"
  }
}
PATCH/integrations/projects/{projectId}/labels

Create or update a single label (upsert by key) on a branch. Translator role or higher; context and tags need the developer role.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
key*stringbodyDotted label key, 1–1000 chars, no empty segments (and no __proto__/constructor/prototype segment).
valuesobjectbody{ langCode: string }. Every language must be configured on the project, else unknown_languages. An empty string empties that language.
statusstringbodyOne of draft, review, approved, rejected.
tagsstring[]bodyReplaces the label's tags. At most 20, each up to 50 characters. Developer role.
contextobjectbody{ description?, charLimit?, location?, doNotTranslate? } – see label context. Developer role.
branchstringbodyBranch 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.

Example response
{
  "key": "brand.name",
  "branch": "main",
  "created": true,
  "values": { "en": "Translatize" },
  "status": "draft",
  "tags": [],
  "context": {
    "description": "Product name in the page header",
    "doNotTranslate": true
  }
}
PATCH/integrations/projects/{projectId}/labels/batch

Upsert 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
labels*object[]bodyArray of { key, values?, status?, tags?, context? }, each with the same rules as the single upsert. Max 500 items, else batch_too_large.
branchstringbodyBranch 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).

Example response
{
  "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.

GET/integrations/projects/{projectId}/branches

List every branch in the project, each flagged with this token's access. main is always listed. Any valid token.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
Example response
{
  "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
    }
  ]
}
POST/integrations/projects/{projectId}/branches

Create 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
name*stringbody1–100 chars matching ^[a-zA-Z0-9_-]+$, and not main (invalid_name). An existing name fails with branch_exists.
descriptionstringbodyOptional, max 500 chars.
basedOnstringbodyOptional 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).

Example response
{
  "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"
  }
}
GET/integrations/projects/{projectId}/branches/{branchName}/compare

Diff 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$.
Example response
{
  "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 }
}
GET/integrations/projects/{projectId}/branches/{branchName}/conflicts

Flat 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$.
Example response
{
  "source": "feat-checkout",
  "target": "main",
  "conflicts": [
    {
      "key": "app.title",
      "lang": "lv",
      "sourceValue": "Veikals",
      "targetValue": "Sākums"
    }
  ],
  "hasConflicts": true,
  "conflictCount": 1
}
POST/integrations/projects/{projectId}/branches/{branchName}/merge

Merge 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$.
strategystringbodyoverwrite (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.
conflictsobject[]bodyPer-conflict resolutions for the manual strategy: { key, lang, resolution: "source"|"target"|"custom", customValue? }. customValue is required with custom.
targetstringbodyOptional and forced to the base branch – any other value returns branch_not_allowed.
bash
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.

Example response
{
  "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
}
POST/integrations/projects/{projectId}/branches/{branchName}/labels/rebase

Mark 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$.
keys*string[]body1 to 500 label keys.
langsstring[]bodyUp 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.

Example response
{
  "rebased": 2,
  "notFound": ["checkout.old_title"]
}
DELETE/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).

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA 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.

Example response
{
  "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.

POST/integrations/projects/{projectId}/translation/auto-translate

Start 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
targetLangs*string[]bodyLanguages to translate into (at least one). Each must be configured on the project, else unknown_target_language.
sourceLangstringbodyLanguage 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.
labelKeysstring[]bodyRestrict to these keys; omit to translate every label needing work.
overwriteTranslatedbooleanbodyRe-translate values that already have a translation. Defaults to false.
branchstringbodyBranch 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.

Example response (202 Accepted)
{
  "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 }
  }
}
GET/integrations/projects/{projectId}/translation/status

The 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.

ParamTypeInDescription
projectId*stringpathThe 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.

Example response
{
  "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.

GET/integrations/projects/{projectId}/glossary

The 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
Example response
{
  "terms": [
    {
      "id": "6659f0c3a1b2c3d4e5f60801",
      "term": "workspace",
      "translations": { "lv": "darbvieta" },
      "description": "A team's shared area",
      "caseSensitive": false,
      "prohibited": false,
      "category": "product",
      "scope": "project"
    }
  ]
}
POST/integrations/projects/{projectId}/tm/matches

Look 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
texts*string[]body1 to 100 source texts, each up to 10,000 characters and 100,000 in total.
sourceLang*stringbodyLanguage of the texts.
targetLang*stringbodyLanguage you want matches in.
thresholdnumberbodyMinimum 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.

Example response
{
  "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.

POST/integrations/projects/{projectId}/qa/run

Run the checks on a branch now. Translator role or higher. Changes no translation.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchstringbodyDefaults to the token's base branch.
keysstring[]bodyLimit 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.

Example response
{
  "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"
    }
  ]
}
GET/integrations/projects/{projectId}/qa/issues

Stored issues of a branch, paged by cursor. Translator role or higher.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchstringqueryDefaults to the token's base branch.
severitystringqueryerror, warning or info.
keystringqueryOnly issues of this key.
langstringqueryOnly issues of this language.
sourcestringqueryrule for the automatic checks, ai for AI review findings.
includeIgnoredbooleanqueryAlso list issues someone chose to ignore (with ignoredReason and ignoredAt).
cursorstringqueryThe nextCursor of the previous page.
limitnumberqueryPage 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.

Example response
{
  "issues": [],
  "nextCursor": null,
  "counts": { "error": 1, "warning": 3, "info": 0 }
}
GET/integrations/projects/{projectId}/branches/{branchName}/merge-gate

Preview the merge gate for merging a branch into the token's base branch, the only target a token merges into. Translator role or higher.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchName*stringpathA branch in the token's allowed set. Must match ^[a-zA-Z0-9_-]+$.
strategystringqueryThe 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.

Example response
{
  "source": "feat-checkout",
  "target": "main",
  "gate": {
    "mode": "block",
    "applies": true,
    "blocked": true,
    "errors": 1,
    "warnings": 3,
    "pairsChecked": 36,
    "issues": []
  }
}
POST/integrations/projects/{projectId}/qa/ai-review

Queue 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
branchstringbodyDefaults to the token's base branch.
keysstring[]bodyLimit the review to these keys (up to 10,000).
langsstring[]bodyLimit 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.

Example response (202 Accepted)
{
  "jobId": "6659f1a2b3c4d5e6f7081930",
  "status": "queued",
  "pairs": 120,
  "estimatedCharacters": 9400
}
GET/integrations/projects/{projectId}/qa/ai-review/status

The 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.

ParamTypeInDescription
projectId*stringpathThe 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.

Example response
{
  "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.

POST/integrations/projects/{projectId}/screenshots/runs

Start 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).

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
name*stringbodyA label for the run, for example the CI job. Cut to 200 characters.
branchstringbodyThe branch whose keys the screens link to. Defaults to the token's base branch.
commitShastringbodyThe commit the captures were taken from (7 to 64 hex characters).
Example response (201 Created)
{ "runId": "6659f1a2b3c4d5e6f7081940" }
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/check

Ask which images the server already holds, so unchanged screens are not uploaded again.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
runId*stringpathThe capture run, from the create call. It must be on a branch the token may use.
clientHashes*string[]bodyUp 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).

Example response
{ "existing": ["9f2c4e…"] }
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/screens

Upload one screen as multipart/form-data. Returns HTTP 201. A run holds at most 500 screens (run_screen_limit).

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
runId*stringpathThe capture run, from the create call. It must be on a branch the token may use.
meta*stringbodyJSON, 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.
imagefilebodyPNG, 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.

Example response (201 Created)
{ "screenshotId": "6659f1a2b3c4d5e6f7081950", "deduped": false, "links": 12 }
POST/integrations/projects/{projectId}/screenshots/runs/{runId}/complete

Finish 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
runId*stringpathThe 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.

Example response
{
  "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).

GET/integrations/projects/{projectId}/ota/distributions

The project's distributions on branches this token may use. Translator role or higher.

ParamTypeInDescription
projectId*stringpathThe 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).

Example response
{
  "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"
    }
  ]
}
POST/integrations/projects/{projectId}/ota/distributions/{distributionId}/releases

Publish 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.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
distributionId*stringpathThe 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).

Example response (201 Created)
{
  "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.

CodeHTTPWhen it happens
access_denied401Missing or malformed Authorization header, an app login token instead of an mcni_ token, or an unknown or revoked token.
access_denied403The token's creator has left the project or now has a role below the token's.
token_expired401The token's expiresAt has passed.
token_inactive403The token's bound branch was deleted; point it at an existing branch to reactivate it (carries reason).
ip_not_allowed403The request IP is not in the token's IP allowlist.
project_mismatch403The {projectId} in the path is not the token's project.
project_not_found404The token's project has been removed.
insufficient_permissions403The token's role is below the route's minimum, or a translator token sent context or tags.
branch_not_allowed400The named branch is not in the token's allowed set (carries boundBranch and allowedBranches).
branch_not_found404 / 409The branch does not exist (409 when a distribution's branch was deleted).
branch_scope_fixed403A fixed-scope token tried to create a branch.
not_token_branch403Merge, rebase or delete targeted a branch this token did not create.
invalid_name400Attempted to create a branch named "main". Other malformed names fail validation and come back in the errors array.
branch_exists400A branch with that name already exists.
branch_limit_reached403The project hit its plan's branch limit (carries limit and current).
source_branch_not_found404The merge-gate preview named a branch that does not exist.
strategy_not_supported_for_branch400branch-changes was asked for with main as the source.
admin_token_required_for_main403A token without the admin role tried to merge into main.
qa_gate_blocked409The QA merge gate blocks the merge (carries gate). A token cannot override it.
qa_gate_unavailable503The gate could not be checked while the mode is block; nothing was merged.
branch_changed_retry409The branch changed between the gate check and the merge; nothing was merged. Retry.
unknown_languages400A label value used a language not configured on the project, or an AI review named one it cannot review (carries languages).
character_limit_exceeded400A value is longer than the label's character limit.
label_not_found404A context- or tags-only write named a key that does not exist.
source_key_limit_reached403New keys would exceed the plan's source-key limit (carries limit and current).
batch_too_large400More than 500 labels in one batch request (carries max: 500).
feature_not_available403The plan does not include the feature – AI translation by default, otherwise named in feature (aiReview, screenshotCapture).
unknown_source_language400The sourceLang of an AI job is not a project language (carries language).
unknown_target_language400A targetLangs entry is not configured on the project (carries languages).
too_many_labels400The job would span more than 100,000 labels (carries limit).
ai_quota_exceeded403The AI character allowance is exhausted (carries used, limit, remaining, estimated).
translation_already_running409A translation job is already running for this project.
nothing_to_review400There is nothing to review in that branch, key and language scope.
too_many_pairs400The AI review scope is too large; narrow it with keys or langs (carries limit and pairs).
ai_review_quota_exceeded403The AI review allowance is exhausted (carries used, limit, remaining, estimated).
ai_review_in_progress409An AI review is already running for this project.
capture_run_limit429More than 30 capture runs in 24 hours (carries limit and Retry-After).
run_not_found404The capture run does not exist in this project.
run_not_open409The capture run is already completed or closed.
run_screen_limit429More than 500 screens in one capture run (carries limit).
invalid_meta400The meta field of a screen upload is missing or not valid JSON.
meta_too_large413The meta field is over 1 MB.
image_required400The screen has no image and the server does not hold one with that clientHash.
invalid_image400The file is not a readable PNG, JPEG or WebP image.
image_too_large413The image is over 10 MB or too large in pixels.
screenshot_quota_exceeded403The account's screenshot storage is full (carries used and limit).
distribution_not_found404The distribution does not exist in this project.
nothing_to_publish422The distribution has nothing to publish (carries publishFilter).
invalid_request400Validation failed on a glossary, memory, QA or screenshot route (with detail or details).
errors400Validation failed on a label, branch or translation route – an array of per-field messages.
rate_limit_exceeded429More 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" }.

Prefer a tool over raw HTTP
You rarely need to call these endpoints by hand. The CLI wraps them for terminals and CI, and the MCP server exposes the same operations to AI agents. Both use the same mcni_ tokens.