API reference
This document matches the behaviour of the Express app in api/app.js and the route handlers under api/routes/.
Limits and behaviour
| Item | Value |
|---|---|
| JSON body size | Up to 2 MB (express.json({ limit: '2mb' })) |
| Targets per request | 1–36 language codes |
| Batch items | 1–100 items per batch request |
| Models | standard (default) or advanced (paid tiers only; see below) |
Monthly token allowance (free tier): Before calling the model, the API estimates tokens as roughly ceil(content_length / 4) × (number_of_targets + 1) and, for free tier only, rejects the request with 429 / token_limit_reached if the estimate would exceed the remaining monthly grant (FREE_TIER_MONTHLY_TOKENS, default 100000). Paid tiers are not blocked by this pre-check in enforceTokenCap; usage is still logged.
Rate limits: When Upstash Redis is configured (UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN, and URL does not contain the placeholder your-instance), per-minute limits apply by tier: free 5, starter 30, growth 60, scale 120, enterprise unlimited. On limit, response is 429 with error: "rate_limit_reached". If Redis is not configured, rate limiting is skipped (see rateLimit.js).
Successful rate-limited responses may include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
GET /health
No authentication.
Response 200
{
"status": "ok",
"timestamp": "2025-03-23T12:00:00.000Z"
}
GET /languages
No authentication.
Returns the canonical list of supported languages (code, display name, RTL flag). There are 36 entries; codes are the only values accepted in targets on translate endpoints.
Response 200
{
"languages": [
{ "code": "en", "name": "English", "rtl": false },
{ "code": "ar", "name": "Arabic", "rtl": true }
]
}
Source: api/utils/languages.js.
POST /translate
Requires Authorization: Bearer <api_key>.
Translates a single content string into every language listed in targets. The model returns a single JSON object whose keys are exactly the requested language codes and whose values are translated strings (see formatPrompts.js).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Non-empty string to translate. |
targets | string[] | Yes | Non-empty array of valid language codes (max 36). |
format | string | No | One of plain, markdown, json, html. If omitted, format is auto-detected from content. |
source | string | No | Source language hint for the model; optional. |
model | string | No | standard (default) or advanced. advanced requires a paid tier (403 on free). |
Response 200
{
"translations": {
"es": "...",
"fr": "..."
},
"usage": {
"input_tokens": 120,
"output_tokens": 340,
"total_tokens": 460,
"model": "standard",
"detected_format": "markdown",
"detection_confidence": 0.95
}
}
detected_format and detection_confidence appear only when format was omitted and auto-detection ran.
Example (cURL)
curl -sS -X POST "https://api.usepolylingo.com/v1/translate" \
-H "Authorization: Bearer $POLYLINGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "{\"title\":\"Hello\"}",
"format": "json",
"targets": ["fr", "de"]
}'
Example (Python 3)
pip install requests
import os, requests
url = "https://api.usepolylingo.com/v1/translate"
headers = {
"Authorization": f"Bearer {os.environ['POLYLINGO_API_KEY']}",
"Content-Type": "application/json",
}
r = requests.post(url, json={
"content": "<p>Hello <strong>world</strong></p>",
"format": "html",
"targets": ["es"],
}, timeout=120)
r.raise_for_status()
print(r.json()["translations"]["es"])
POST /translate/batch
Requires Authorization: Bearer <api_key>.
Processes each item sequentially (one model call per item). If any item fails, the API returns 500 and does not return partial results for that request.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Each element: id (string), content (string), optional format. |
targets | string[] | Yes | Same rules as /translate. |
source | string | No | Optional source language hint. |
model | string | No | standard or advanced (same rules as /translate). |
Response 200
{
"results": [
{ "id": "welcome", "translations": { "fr": "...", "de": "..." } },
{ "id": "goodbye", "translations": { "fr": "...", "de": "..." } }
],
"usage": {
"total_tokens": 900,
"input_tokens": 400,
"output_tokens": 500,
"model": "standard"
}
}
Example
curl -sS -X POST "https://api.usepolylingo.com/v1/translate/batch" \
-H "Authorization: Bearer $POLYLINGO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "id": "a", "content": "Hello", "format": "plain" },
{ "id": "b", "content": "## Title", "format": "markdown" }
],
"targets": ["es", "it"]
}'
POST /jobs
Requires Authorization: Bearer <api_key>.
Enqueues a translation job and returns immediately with a job_id. The translation runs in the background — no HTTP timeout risk regardless of content size. Poll GET /jobs/:id for the result.
Use this endpoint instead of POST /translate when translating large documents (long Markdown, many target languages) where the request duration might exceed your HTTP client or proxy timeout.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Non-empty string to translate. |
targets | string[] | Yes | Non-empty array of valid language codes (max 36). |
format | string | No | One of plain, markdown, json, html. Auto-detected if omitted. |
source | string | No | Source language hint; optional. |
model | string | No | standard (default) or advanced. |
Response 202
{
"job_id": "a1b2c3d4-...",
"status": "pending",
"created_at": "2025-03-23T12:00:00.000Z"
}
GET /jobs/:id
Requires Authorization: Bearer <api_key>.
Polls the status of a job submitted via POST /jobs. Poll every 5–10 seconds. Jobs are owned by the submitting user — other users receive 404.
Response (pending / processing)
{
"job_id": "a1b2c3d4-...",
"status": "pending",
"created_at": "2025-03-23T12:00:00.000Z",
"updated_at": "2025-03-23T12:00:00.000Z",
"completed_at": null,
"queue_position": 3
}
status is pending (waiting for a worker) or processing (worker has claimed it). queue_position (1-based) is how many pending or processing jobs were created strictly before this one — use it for progress UI. Omitted when the count query fails.
Response (completed)
{
"job_id": "a1b2c3d4-...",
"status": "completed",
"created_at": "2025-03-23T12:00:00.000Z",
"updated_at": "2025-03-23T12:00:02.000Z",
"completed_at": "2025-03-23T12:00:02.000Z",
"translations": {
"es": "...",
"fr": "..."
},
"usage": {
"input_tokens": 120,
"output_tokens": 340,
"total_tokens": 460,
"model": "standard"
}
}
Response (failed)
{
"job_id": "a1b2c3d4-...",
"status": "failed",
"error": "Model returned invalid JSON"
}
Example (JavaScript)
const API = 'https://api.usepolylingo.com/v1'
const headers = {
'Authorization': `Bearer ${process.env.POLYLINGO_API_KEY}`,
'Content-Type': 'application/json',
}
// 1. Submit
const submit = await fetch(`${API}/jobs`, {
method: 'POST',
headers,
body: JSON.stringify({ content: longMarkdown, format: 'markdown', targets: ['de', 'fr'] }),
})
const { job_id } = await submit.json()
// 2. Poll
while (true) {
await new Promise(r => setTimeout(r, 10_000))
const poll = await fetch(`${API}/jobs/${job_id}`, { headers })
const job = await poll.json()
if (job.status === 'completed') { console.log(job.translations); break }
if (job.status === 'failed') { throw new Error(job.error) }
// Optional: show progress (queue_position is 1-based, omitted when not queued)
if (job.queue_position != null) console.log(`Queue position: ${job.queue_position}`)
}
GET /usage
Requires Authorization: Bearer <api_key> (standard key lookup — not the internal bypass-only path).
Returns token usage for the current calendar month for the authenticated user.
Response 200
{
"period_start": "2025-03-01T00:00:00.000Z",
"period_end": "2025-03-31T23:59:59.000Z",
"tokens_used": 12000,
"tokens_included": 100000,
"tokens_remaining": 88000,
"overage_tokens": 0,
"tier": "free"
}
tokens_included and tokens_remaining are null for enterprise (unlimited grant in reporting).
Content formats
Supported format values: plain, markdown, json, html.
| Format | Preserved | Translated |
|---|---|---|
plain | Line breaks / paragraphs | All visible text |
markdown | Syntax, links (URL unchanged), fenced code (verbatim) | Prose and link text |
json | Keys, structure, non-string types | String values only |
html | Tags and attributes | Text nodes and appropriate attributes (see prompts) |
RTL and direction in your app
For plain and markdown output, the API returns translated text only — it does not add dir="rtl" or wrapper elements. Set text direction in your UI (CSS direction, a parent element’s dir attribute, or your framework’s i18n layout) when displaying Arabic, Hebrew, or Persian.
For html format, translated markup may include dir="rtl" where appropriate for RTL targets; see formatPrompts.js and the HTML tests in scripts/test-translation.js.
Error responses
Errors are JSON when possible:
{
"error": "invalid_request",
"message": "Human-readable detail"
}
| HTTP | error | When |
|---|---|---|
| 400 | invalid_request | Missing/invalid body fields (e.g. empty content, bad targets) |
| 400 | invalid_format | format not in the supported set |
| 400 | invalid_language | Unknown code in targets |
| 401 | invalid_api_key | Missing/malformed Authorization, unknown key, revoked key |
| 403 | advanced_not_available | model: "advanced" on free tier |
| 429 | token_limit_reached | Free tier monthly cap would be exceeded (pre-check) |
| 429 | rate_limit_reached | Per-minute RPM limit (when Redis enabled) |
| 500 | translation_error | Model/network failure; safe to retry |
| 404 | not_found | GET /jobs/:id — job does not exist or belongs to another user |
| 500 | server_error | POST /jobs — failed to enqueue; safe to retry |
GET /usage may return 500 with a generic message if Supabase query fails.
Underlying models (informational)
The API exposes only standard and advanced. Actual OpenAI model IDs are configured in api/utils/modelRouter.js and are not returned in API responses.