Operations and approvals
An operation is a request for a person's approval. A key created as AI agent cannot spend credits, publish or roll back on its own: it describes what it wants to do, a person approves it in the dashboard, and the key then runs that one thing under the approval. The server enforces this for the key itself, whatever client holds it. Approvals and the agent key covers the dashboard side: how to create the key and who can approve.
The MCP server wraps this flow in its authorize
tool. Use the routes directly when you build your own agent or tool.
What needs an operation
| Request by an agent key | Without an operation |
|---|---|
POST /translate or POST /sync that queues paid translation | 403 authorization_required, with the estimatedCredits to ask for |
POST /publish | 403 authorization_required |
POST /versions/{versionId}/restore | 403 authorization_required |
POST /content/{docId}/run | 403 authorization_required always: no operation covers a content run |
Everything else runs as with any key: reads, previews with dryRun, adding a
language, and a push or a sync that costs nothing.
A key that is not an agent key may send an operationId too. The request is
then held to the same checks.
GET /credential tells a client which kind of key it holds; see
Scopes.
Ask for an approval
The two /operations routes are the only ones here without the project in
their path. A request names its project in scope.project_id, and the key
needs the publish scope for a publish or a rollback and translate for a
translation.
curl -X POST "https://api.easyi18n.com/v1/operations" \
-H "Authorization: Bearer $EASYI18N_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action": "publish",
"scope": {
"project_id": "p7Hq2mXc",
"approved_only": false,
"require_complete": true,
"incomplete_mode": "skip",
"expected_pending": 0
}
}'
{
"operation_id": "Qe6rT1uVw8",
"action": "publish",
"state": "pending_approval",
"scope": {
"project_id": "p7Hq2mXc",
"approved_only": false,
"require_complete": true,
"incomplete_mode": "skip",
"expected_pending": 0
},
"requested_mc": 0,
"authorized_mc": 0,
"reserved_mc": 0,
"charged_mc": 0,
"absorbed_mc": 0,
"approval_url": "https://app.easyi18n.com/approve/Qe6rT1uVw8",
"expires_at": "2026-10-11T14:32:07Z",
"attempts": []
}
Show approval_url to the person. It opens the approval screen of the
dashboard, with the scope they are approving. Creating the operation starts no
work and reserves no credits.
Asking again with the same action and scope while the first request is pending returns the same operation. It does not extend its expiry.
Then poll until the state changes:
curl "https://api.easyi18n.com/v1/operations/Qe6rT1uVw8" \
-H "Authorization: Bearer $EASYI18N_TOKEN"
state | Meaning |
|---|---|
pending_approval | Waiting for a person. Expires 30 minutes after it was created. |
approved | Ready to use. |
denied | The person refused. |
expired | Nobody answered in time, or the approval was not used in time. |
exhausted | A publish or a rollback already used it. |
Only the key that created an operation can read it. Any other key, an unknown
ID and a deleted project all answer 404 operation_not_found.
Publish or roll back under an approval
Send the request with operationId and the same options the scope named:
curl -X POST "https://api.easyi18n.com/v1/projects/p7Hq2mXc/publish" \
-H "Authorization: Bearer $EASYI18N_TOKEN" \
-H "Content-Type: application/json" \
-d '{"operationId": "Qe6rT1uVw8", "approvedOnly": false, "require_complete": true, "incompleteMode": "skip"}'
The body's approvedOnly, require_complete and incompleteMode have to
equal the scope's approved_only, require_complete and incomplete_mode.
State incompleteMode in the body: an omitted one matches no scope. A
difference is 409 scope_changed, because the person approved something else.
The approval is good for one publish and for 30 minutes after it was given. A
publish that is refused without creating a version, for example with
409 fill_incomplete, gives the use back, so the agent can fix the cause and
try again under the same approval.
A rollback names the two versions in its scope:
{
"action": "rollback",
"scope": {"project_id": "p7Hq2mXc", "from_version": "current", "to_version": "previous"}
}
current and previous are resolved when the operation is created, so the
approval screen shows real version IDs. The restore runs only while
from_version is still the current version and the request restores
to_version. If someone published in between, it is 409 scope_changed.
Translate under an approved budget
A translation is approved for an exact payload and a maximum number of credits. The agent cannot change the strings after the person approved them.
-
Split the work into batches, each a body for
POST /translateorPOST /sync, and preview every batch with?dryRun=true. Each preview returnsestimatedCreditsand abatchSha256. -
Ask for the operation.
max_creditsis in whole credits. The scope lists the target languages, the batches in order, and a hash of that list:{"action": "translate","max_credits": 3,"scope": {"project_id": "p7Hq2mXc","langs": ["es", "fr"],"unit_count": 2,"batches": [{"index": 0, "sha256": "4be1…", "unit_count": 2}],"payload_sha256": "c07a…"}}payload_sha256is the SHA-256 of{"batches":[…]}serialized with every object's keys sorted and no whitespace.langshas to name target languages of the project. -
Once it is
approved, send each batch with five fields added to its body:operationId, arequestId, anattemptIdshared by all the batches of this run, itsbatchIndexand itsbatchSha256.
A batch that is not the approved one is refused: 409 batch_mismatch when the
body does not hash to the approved value, 409 batch_out_of_range for an
index outside the list.
The operation's numbers are in millicredits. authorized_mc is what the
person approved, reserved_mc what running work holds, charged_mc what was
spent. Charges never pass authorized_mc. When nothing is left the next batch
is 409 budget_exhausted, and units the run could not reach are reported as
budgetExhausted on the next push: ask for a new operation instead of
retrying.
An operation runs at most three attempts, each under its own attemptId; a
fourth is 409 attempts_exhausted. Within an attempt a batch is sent once. If
its request failed, send the batch again under a new requestId with the same
attemptId and batchIndex; a second live request for the same batch is
409 batch_replayed. attempts in the operation lists the requests seated
on each batch and their state, the same states
GET /requests/{requestId}
returns.
An approved translation expires after 30 minutes without use and with no job of its own still running.
When a request under an operation is refused
| Status | error | Meaning |
|---|---|---|
| 403 | authorization_required | An agent key sent no operationId. |
| 404 | operation_not_found | No such operation for this key. |
| 409 | operation_not_approved | The operation is pending, denied, expired or already used. |
| 409 | scope_changed | The request is not what the operation approved. Ask for a new one. |
| 409 | budget_exhausted | The approved credits are spent or reserved. |
| 503 | unavailable | The operation changed while it was being created. Send the same request again. |