Skip to main content

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.

ScopeRoutes
readGET /translations, GET /meta, GET /pending-code-changes, GET /content, GET /content/{docId}
translatePOST /translate, POST /sync, POST /content/{docId}/run, POST /operations for a translation
writePOST /languages, POST /output-formats, POST /content, PUT /content/{docId}
publishPOST /publish, POST /versions/{versionId}/restore, every /webhooks route, POST /operations for a publish or a rollback
capturePOST /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.

StatuserrorMeaning
400invalid_argument and route-specific codesThe request is malformed. Nothing was written.
401unauthorizedThe key is missing, invalid or belongs to another project.
402insufficient_credits, spend_cap_exceeded, plan_limitThe workspace balance, its monthly spending cap or its plan refused the work.
403forbidden, authorization_requiredThe key lacks the scope, or it is an agent key and the request needs an approved operation.
404not_found and route-specific codesThe project, version, document or key does not exist.
409route-specificThe request conflicts with the current state. Each workflow page lists its codes.
413payload_too_largeThe body is over the route's size cap.
429rate_limited and the daily quota codesA request budget is used up. See Limits.
500internalAn unexpected failure. The write may or may not have landed.
503unavailableThe 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:

RoutePer dayWhen it is used up
POST /publish30 publishes by API key429 rate_limited
POST /versions/{versionId}/restore30 restores by API key, counted apart from publishes429 rate_limited
POST /capture5,000 new keys429 capture_quota_exceeded
POST /content, PUT /content/{docId}500 imports429 import_quota_exceeded
POST /content/{docId}/run500 runs429 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.