REST API

Rate limits

Stay inside the per-token request limits and back off correctly when you reach one.

The API limits how many requests a credential can make per minute. Limits are counted per deploy token, so two tokens on the same project each have their own budget.

The limits

RequestsLimitCounted per
Every /v1 endpoint not listed below60 per minuteDeploy token
POST /v1/push300 per minuteDeploy token, in its own budget
POST /v1/batches/{batch}/cancel60 per minuteDeploy token, in its own budget
GET /v1/cdn/{project}/{locale} and .../v{version}300 per minuteCDN key and client IP address

Push and cancel draw on budgets of their own, so they never compete with the general one:

  • A repository upload is one push per file, so a project with many locale files can upload them all without touching the budget your status checks use.
  • Polling a batch never uses up the requests you need to cancel it.

Headers

Responses from a limited endpoint carry the current state of the limit:

HeaderOnMeaning
X-RateLimit-LimitEvery response to an authenticated requestRequests allowed per minute
X-RateLimit-RemainingEvery response to an authenticated requestRequests left in the current minute
Retry-After429 onlySeconds to wait before the next request will be accepted
X-RateLimit-Reset429 onlyUnix time, in seconds, when the limit resets

When you exceed a limit, the answer is 429 with the problem type /problems/rate-limited:

429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791820920

{"type":"/problems/rate-limited","title":"Rate limited","status":429,"detail":"Too many requests."}

Not every 429 is a rate limit

/problems/quota-exceeded and /problems/extract-cap-reached are also 429, but they mean the account has used its AI allowance for the cycle. They carry no Retry-After and do not clear in seconds; resets_at in the body says when they lift. Branch on type before you retry. See Errors.

Backing off

Wait for Retry-After when it is present. When it is not (a 5xx, or a dropped connection), back off exponentially with jitter, and give up after a few attempts.

request-with-backoff.ts
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function requestWithBackoff(url: string, init: RequestInit, attempts = 5): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, init);

    const retryable =
      res.status >= 500 ||
      (res.status === 429 && (await res.clone().json()).type === '/problems/rate-limited');

    if (!retryable || attempt === attempts - 1) return res;

    const retryAfter = Number(res.headers.get('Retry-After'));
    const delay = retryAfter > 0
      ? retryAfter * 1000
      : Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 250;

    await sleep(delay);
  }
}

Recent versions of curl honour Retry-After on a 429 when you ask them to retry:

curl with retries
curl --retry 4 --retry-max-time 120 \
  https://api.transglot.ai/v1/batches/4822 \
  -H "Authorization: Bearer $TRANSGLOT_TOKEN"

Staying under the limits

  • Poll with backoff. When you watch a batch, start at one second and double the interval up to about fifteen. A run takes minutes, not seconds.
  • Use webhooks instead of polling. Subscribe an endpoint to batch.completed and batch.failed and you need no status requests at all. See Webhooks.
  • Read many values at once. POST /v1/translations/batch reads up to 1,000 keys in one request where GET /v1/translations reads one.
  • Revalidate instead of downloading. Send the ETag you have as If-None-Match on GET /v1/pull and the CDN reads. A 304 is still a request, but it carries no body.
  • One token per pipeline. Each token has its own budget, so a busy CI pipeline does not slow down another job that uses a different token.

The command line tool already waits for Retry-After and backs off while it polls.

On this page