Integration API
Every Translatize integration – the CLI, the CI actions, and the MCP server – is built on this token-authenticated REST API. It exposes labels, exports, git-like branches, and AI translation over one bearer-token surface.
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.
Roles
The role hierarchy is owner ≥ admin ≥ developer ≥ translator. Reading and writing labels needs translator or higher; creating, merging, and deleting branches needs developer or higher. Token introspection, listing branches, comparing, conflict checks, and translation status work with any valid token. A role that is too low is rejected with insufficient_permissions.
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 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. Schema-validation failures instead return an errors array of per-field messages. See the full error-code table.
Token introspection
/integrations/meDescribe the calling token: its project, base branch, branch scope, role, and non-secret settings. No role gate – any valid token may introspect itself. Secrets (token hash, IP allowlist, webhook ids) are never returned.
{
"project": {
"id": "6659f0c3a1b2c3d4e5f60718",
"name": "Acme Web",
"langs": ["en", "lv", "ru"]
},
"branch": "main",
"branchScope": "fixed",
"role": "developer",
"token": {
"name": "ci-token",
"autoPublish": false,
"expiresAt": null
}
}Labels
/integrations/projects/{projectId}/labelsList labels on a branch, sorted by key ascending. Requires the 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": 3,
"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. Requires the 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. Requires the 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. |
| key* | string | body | Dotted label key, 1–1000 chars, no empty segments (and not __proto__/constructor/prototype). |
| values | object | body | { langCode: string }. Every language must be configured on the project, else unknown_languages. |
| tags | string[] | body | Tags to set on the label. |
| status | string | body | One of draft, review, approved, rejected. |
| branch | string | body | Branch to write to. Defaults to the base branch. |
At least one of values, tags, or status is required. created is true when the key did not previously exist on the branch.
{
"key": "greeting.hello",
"branch": "main",
"created": true,
"values": { "en": "Hello" },
"status": "approved",
"tags": ["ui"]
}/integrations/projects/{projectId}/labels/batchUpsert up to 500 labels in one call. Each item is upserted independently – a rejected item lands in failed[] instead of aborting the batch. Requires the 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. |
| labels* | object[] | body | Array of { key, values?, tags?, status? }. Max 500 items, else batch_too_large. |
| branch | string | body | Branch to write to. Defaults to the base branch. |
Failures are collected per key, e.g. { "key": "bad.item", "error": "unknown_languages" } – the rest of the batch still applies.
{
"updated": 0,
"created": 2,
"failed": []
}Branches
Branch reads work with any token; writes (create / merge / delete) require a create-own token with the developer role. 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. Requires 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. |
| 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).
{
"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 base branch. Requires a create-own token with the developer role; the target is implicitly the base. The source branch is NOT deleted.
| 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, non-destructive union – source wins conflicts), keep-newer, manual, or replace (destructive – base becomes a copy of source). |
| conflicts | object[] | body | Per-conflict resolutions for the manual strategy: { key, lang, resolution: "source"|"target"|"custom", customValue? }. |
| target | string | body | Optional and forced to the base branch – any other value returns branch_not_allowed. |
{
"message": "Branch merged successfully",
"source": "feat-checkout",
"target": "main",
"strategy": "overwrite"
}/integrations/projects/{projectId}/branches/{branchName}Delete a branch this token created – cleanup after a merge. Requires the developer role. The base branch, main, and branches created by others 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_-]+$. |
{
"message": "Branch deleted successfully"
}AI translation
Platform AI auto-translation is a paid-plan feature (Professional and Agency). On a plan without it the launch returns feature_not_available – fall back to exporting the missing values and translating them yourself.
/integrations/projects/{projectId}/translation/auto-translateLaunch a background AI job to fill 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 (plan-gated).
| 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 first 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", ... }. Quota exhaustion returns ai_quota_exceeded (with used/limit/remaining), and a job already in flight returns translation_already_running. Poll 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
},
"chars": { "estimated": 840, "actual": 0 }
}
}/integrations/projects/{projectId}/translation/statusThe project's live translation job (or null), the most-recent job for context, and the AI quota of the subscription governing the project. Any valid token.
| Param | Type | In | Description |
|---|---|---|---|
| projectId* | string | path | The project the token is bound to. A mismatch is rejected with project_mismatch. |
In aiQuota, a limit of -1 means unlimited. While a job is queued, activeJob carries its position in the global FIFO queue.
{
"activeJob": null,
"lastJob": {
"id": "6659f1a2b3c4d5e6f7081920",
"status": "completed",
"branch": "main",
"sourceLang": "en",
"targetLangs": ["lv", "ru"],
"counts": {
"requestedLabels": 12,
"translatedValues": 24,
"failedValues": 0,
"skippedValues": 0
},
"chars": { "estimated": 840, "actual": 812 },
"error": null,
"startedAt": "2026-07-19T09:25:00.000Z",
"finishedAt": "2026-07-19T09:25:48.311Z"
},
"aiQuota": { "used": 812, "limit": 100000, "remaining": 99188 }
}Error codes
Semantic errors carry a machine-readable error code; some add context fields (shown in the notes). Schema-validation failures return an errors array instead.
| Code | HTTP | When it happens |
|---|---|---|
| access_denied | 401 | Missing/malformed Authorization header, a platform JWT instead of an mcni_ token, or an unknown/revoked token. |
| token_expired | 401 | The token's expiresAt has passed. |
| token_inactive | 403 | The token's bound branch was deleted; re-point it at an existing branch to reactivate (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 (translator / developer). |
| branch_not_allowed | 400 | The named branch is not in the token's allowed set (carries boundBranch + allowedBranches). |
| branch_not_found | 404 | The resolved branch does not exist on the project. |
| branch_scope_fixed | 403 | A fixed-scope token tried to create a branch. |
| not_token_branch | 403 | Merge/delete targeted a branch this token did not create. |
| invalid_name | 400 | Attempted to create a branch named "main" (the reserved default branch). Other malformed names – illegal characters or over 100 chars – fail schema validation and come back in the errors array instead. |
| branch_exists | 400 | A branch with that name already exists. |
| branch_limit_reached | 403 | The project hit its plan's branch limit (carries limit + current). |
| unknown_languages | 400 | A label value used a language not configured on the project. |
| character_limit_exceeded | 400 | A value exceeded the label's configured character limit. |
| batch_too_large | 400 | More than 500 labels in one batch request (carries max: 500). |
| feature_not_available | 403 | The project's plan does not include platform AI auto-translation. |
| 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 quota is exhausted (carries used, limit, remaining, estimated). |
| translation_already_running | 409 | A translation job is already running for this project. |
| errors | 400 | Request body or query failed schema validation – an array of per-field messages. |
| rate_limit_exceeded | 429 | More than 2,000 requests from one IP in a 15-minute window. |
Limits
Batch upserts are capped at 500 labels per request. A single AI job may span at most 100,000 labels. AI translation is metered in characters against the governing subscription's monthly quota (a limit of -1 is unlimited). The whole integration surface is rate-limited to 2,000 requests per IP per 15 minutes; exceeding it returns HTTP 429 with { "error": "rate_limit_exceeded" }.
mcni_ tokens.