REST API
The public API lives at https://api.easyi18n.com/v1. The
CLI and the MCP server are
clients of it, so anything they do can be done with plain HTTP requests. Use
it when your stack has no adapter, or when a CMS or a build system has to talk
to easyi18n without a person in between.
The pages of this section each follow one workflow:
- Translate and sync registers source strings, prices the translation and polls it.
- Publish and delivery freezes a version, rolls back, and reads the published files, the manifest and the bundles.
- Content pushes long-form documents and pulls their published translations.
- Webhooks notifies your endpoint when a version is published.
- Operations asks a person to approve a spend, a publish or a rollback.
Field by field, the reference is the OpenAPI document. Load it into any OpenAPI viewer or client generator.
Authentication
Create a key in the dashboard under the project's Settings → API keys → New
API key. Give it a label, pick its scopes and choose who uses it: CI /
scripts for a key that acts on its own, AI agent for a key that needs a
person's approval before it spends or publishes. The key
starts with eik_ and the dashboard shows it once.
Send it as a bearer credential:
curl https://api.easyi18n.com/v1/projects/$PROJECT_ID/meta \
-H "Authorization: Bearer $EASYI18N_TOKEN"
A key belongs to one project. A request without a key, or with a key of
another project, is 401 unauthorized. A key that lacks the scope a route
needs is 403 forbidden.
The delivery routes take no key: the manifest, the bundles and /health are
public, because published bundles ship inside apps.
Find the project ID
Paths carry the project ID, not its name. Resolve it once from the workspace
handle and the project slug, the @acme/mobile-app pair of the dashboard URL:
curl "https://api.easyi18n.com/v1/projects/resolve?workspace=acme&project=mobile-app" \
-H "Authorization: Bearer $EASYI18N_TOKEN"
{"projectId": "p7Hq2mXc", "workspaceId": "w3Lk9tRa"}
Any key of that project can call it, whatever its scopes.
Scopes
Scopes are checked exactly. Holding one never implies another, so pick every
scope the integration needs when you create the key. admin is the exception:
it grants all of them.
| Scope | Routes |
|---|---|
read | GET /translations, GET /meta, GET /pending-code-changes, GET /content, GET /content/{docId} |
translate | POST /translate, POST /sync, POST /content/{docId}/run, POST /operations for a translation |
write | POST /languages, POST /output-formats, POST /content, PUT /content/{docId} |
publish | POST /publish, POST /versions/{versionId}/restore, every /webhooks route, POST /operations for a publish or a rollback |
capture | POST /capture |
Three reads accept more than one scope. Polling a translation job
(GET /translations?trackingToken=) and reading one key
(GET /translations?ref=) accept read or translate. GET /projects/resolve,
GET /requests/{requestId}, GET /credential and GET /operations/{operationId}
accept any key of the project.
The dialog also offers Read members and Manage members. No route of this API checks them.
To see what a key holds, ask the API:
curl https://api.easyi18n.com/v1/projects/$PROJECT_ID/credential \
-H "Authorization: Bearer $EASYI18N_TOKEN"
{"scopes": ["read", "translate"], "requires_operation_approval": false}
requires_operation_approval is true for a key created as AI agent.
Errors
Every error is a JSON object with a machine-readable error and a message
for people, under the matching HTTP status:
{"error": "forbidden", "message": "credential lacks publish scope"}
Branch on error, not on message. Some refusals add the numbers they were
decided on: insufficient_credits carries availableCredits,
spend_cap_exceeded carries capRemaining and spendingCap, plan_limit
carries details with the feature and its limit.
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_argument and route-specific codes | The request is malformed. Nothing was written. |
| 401 | unauthorized | The key is missing, invalid or belongs to another project. |
| 402 | insufficient_credits, spend_cap_exceeded, plan_limit | The workspace balance, its monthly spending cap or its plan refused the work. |
| 403 | forbidden, authorization_required | The key lacks the scope, or it is an agent key and the request needs an approved operation. |
| 404 | not_found and route-specific codes | The project, version, document or key does not exist. |
| 409 | route-specific | The request conflicts with the current state. Each workflow page lists its codes. |
| 413 | payload_too_large | The body is over the route's size cap. |
| 429 | rate_limited and the daily quota codes | A request budget is used up. See Limits. |
| 500 | internal | An unexpected failure. The write may or may not have landed. |
| 503 | unavailable | The datastore refused a write, and the message says what to run again. |
After a 503 from POST /translate or POST /sync, run the push or the sync
again from your source files instead of resending the same request: both
classify their whole input again. When the message names a credit hold the
failed attempt could not release, the hold clears on its own within about 35
minutes.
Every response carries an x-correlation-id header. Quote it when you contact
support.
Credits and previews
Credit amounts in responses (estimatedCredits, creditsCharged, balance)
are integers in millicredits: 1,000 millicredits are one credit as the
dashboard shows it. POST /operations is the exception on the way in: its
max_credits is in whole credits.
The routes that spend credits can price the work first. The flag is spelled
dryRun, exactly. On POST /translate and POST /sync it goes in the query
string as ?dryRun=true. On POST /content/{docId}/run it goes in the JSON
body as a boolean. A flag in the wrong place, a different spelling such as
dry_run, or a value such as 1 or "true" where a boolean is expected is
400 invalid_argument. The API refuses what it cannot read instead of running
for real. POST /publish, the restore, POST /capture, POST /languages,
POST /output-formats and the two Content pushes have no preview and answer
400 to the flag. No other route checks for it, so do not send it to the
/webhooks or /operations routes expecting a preview.
Limits
Requests to one project share a budget of 120 per 60-second window. It counts
every request under /projects/{projectId}/, reads and the public delivery
routes included, and the authenticated /operations requests. Requests to the
/@{handle}/{slug}/ form of a project are counted in a window of their own.
Over the budget, the API answers 429 rate_limited with Retry-After: 60.
The count is approximate when requests arrive at the same instant, so treat
120 as the ceiling to stay under, not a number to reach.
Some writes also have a budget per project and day:
| Route | Per day | When it is used up |
|---|---|---|
POST /publish | 30 publishes by API key | 429 rate_limited |
POST /versions/{versionId}/restore | 30 restores by API key, counted apart from publishes | 429 rate_limited |
POST /capture | 5,000 new keys | 429 capture_quota_exceeded |
POST /content, PUT /content/{docId} | 500 imports | 429 import_quota_exceeded |
POST /content/{docId}/run | 500 runs | 429 run_quota_exceeded |
Publishes and restores made in the dashboard do not count against the API budgets. The capture and Content budgets reset at 00:00 UTC.
Request bodies are capped at 1 MiB. POST /sync takes 4 MiB, for a file of at
most 2 MiB once decoded. The Content import routes take 256 KB and
POST /content/{docId}/run takes 8 KB. A larger body is 413.