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.

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.

Roles

The role hierarchy is owneradmin developertranslator. 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

GET/integrations/me

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

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

GET/integrations/projects/{projectId}/labels

List labels on a branch, sorted by key ascending. Requires the 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": 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"
    }
  ]
}
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. Requires the translator role or higher.

ParamTypeInDescription
projectId*stringpathThe project the token is bound to. A mismatch is rejected with project_mismatch.
formatstringqueryjson (default), csv, ios, or android. The ios/android formats are single-language and default to the project's first language when lang is omitted.
langstringqueryRestrict the export to a single 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. Requires the translator role or higher.

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 not __proto__/constructor/prototype).
valuesobjectbody{ langCode: string }. Every language must be configured on the project, else unknown_languages.
tagsstring[]bodyTags to set on the label.
statusstringbodyOne of draft, review, approved, rejected.
branchstringbodyBranch 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.

Example response
{
  "key": "greeting.hello",
  "branch": "main",
  "created": true,
  "values": { "en": "Hello" },
  "status": "approved",
  "tags": ["ui"]
}
PATCH/integrations/projects/{projectId}/labels/batch

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

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

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

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

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 base branch. Requires a create-own token with the developer role; the target is implicitly the base. The source branch is NOT deleted.

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, non-destructive union – source wins conflicts), keep-newer, manual, or replace (destructive – base becomes a copy of source).
conflictsobject[]bodyPer-conflict resolutions for the manual strategy: { key, lang, resolution: "source"|"target"|"custom", customValue? }.
targetstringbodyOptional and forced to the base branch – any other value returns branch_not_allowed.
Example response
{
  "message": "Branch merged successfully",
  "source": "feat-checkout",
  "target": "main",
  "strategy": "overwrite"
}
DELETE/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).

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

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

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

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 first 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", ... }. 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.

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
    },
    "chars": { "estimated": 840, "actual": 0 }
  }
}
GET/integrations/projects/{projectId}/translation/status

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

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

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

CodeHTTPWhen it happens
access_denied401Missing/malformed Authorization header, a platform JWT instead of an mcni_ token, or an unknown/revoked token.
token_expired401The token's expiresAt has passed.
token_inactive403The token's bound branch was deleted; re-point it at an existing branch to reactivate (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 (translator / developer).
branch_not_allowed400The named branch is not in the token's allowed set (carries boundBranch + allowedBranches).
branch_not_found404The resolved branch does not exist on the project.
branch_scope_fixed403A fixed-scope token tried to create a branch.
not_token_branch403Merge/delete targeted a branch this token did not create.
invalid_name400Attempted 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_exists400A branch with that name already exists.
branch_limit_reached403The project hit its plan's branch limit (carries limit + current).
unknown_languages400A label value used a language not configured on the project.
character_limit_exceeded400A value exceeded the label's configured character limit.
batch_too_large400More than 500 labels in one batch request (carries max: 500).
feature_not_available403The project's plan does not include platform AI auto-translation.
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 quota is exhausted (carries used, limit, remaining, estimated).
translation_already_running409A translation job is already running for this project.
errors400Request body or query failed schema validation – an array of per-field messages.
rate_limit_exceeded429More 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" }.

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.