On this page

Polytranslator API

Translate documents and text in 390+ languages with one API key. Upload a PDF, Word file, slide deck, spreadsheet, ebook, scan or photo and get the same kind of file back in the target language, with its layout preserved. Pay with prepaid credits; no subscription is required.

Base URL: https://www.polytranslator.com/api/v1

At a glance

DocumentsText
EndpointPOST /documents (async job)POST /translate (synchronous)
InputPDF (native or scanned), DOCX, PPTX, XLSX, EPUB, PNG/JPG/WEBP/HEIC, plus DOC, ODT, RTF, TXT, MD, HTML, SRT, VTT, XLS, ODS, CSV, PPT, ODP, Pages, Numbers, Key, MOBI, AZW3, FB2Up to 50,000 characters
OutputSame file type, layout preserved (output=formatted), or translated text only (output=text)Translated text
Size limits50 MB, 1,000 PDF pages, 1,000,000 characters per file50,000 characters per request
PriceQuoted in credits before you pay (free quote); charged exactly the quote1 credit per 1,000 characters (Fast), 3 per 1,000 (Advanced)
Languages390+, including Ancient Greek, Latin, Coptic, Old English, Old Norse, Aramaic, Akkadian, Sumerian and Classical Chinese; source language auto-detectedSame

Both endpoints offer two tiers: fast and advanced (the most accurate engine; costs more credits).

Quickstart: translate a document

  1. Get an API key and buy a credit pack in the dashboard (or let an agent do it headlessly; see Choose an onboarding path).
  2. Submit the file. The response is 202 Accepted with a document id.
sh
curl https://www.polytranslator.com/api/v1/documents \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F file=@contract.pdf \
  -F tgt=spa_Latn
  1. Wait for it to finish (wait holds the request open for up to 60 seconds), then download the translated file:
sh
curl "https://www.polytranslator.com/api/v1/documents/$DOCUMENT_ID?wait=60" \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY"

curl -o contract.es.pdf \
  https://www.polytranslator.com/api/v1/documents/$DOCUMENT_ID/file \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY"

Python

python
import os, time, uuid, requests

BASE = "https://www.polytranslator.com/api/v1"
auth = {"Authorization": "Bearer " + os.environ["POLYTRANSLATOR_API_KEY"]}

# Keep this UUID with the file: retrying with it never creates a second job.
request_id = str(uuid.uuid4())
with open("contract.pdf", "rb") as f:
    doc = requests.post(
        BASE + "/documents",
        headers={**auth, "Idempotency-Key": request_id},
        files={"file": f},
        data={"tgt": "spa_Latn", "max_credits": 500},
        timeout=300,
    ).json()

while doc["status"] in ("queued", "processing"):
    doc = requests.get(f"{BASE}/documents/{doc['id']}?wait=60", headers=auth, timeout=90).json()

if doc["status"] != "completed":
    raise SystemExit(doc["error"])
with open("contract.es.pdf", "wb") as out:
    out.write(requests.get(doc["files"]["document"], headers=auth, timeout=300).content)

Documents

Supported formats and output

UploadReturned file (output=formatted)
PDF (text-based or scanned)PDF
DOCX, PPTX, XLSX, EPUBSame format
PNG, JPG/JPEG, WEBP, HEIC photos and screenshotsPNG
DOC, ODT, RTF, TXT, MD, HTML, XML, SRT, VTT, TEX, Pages, MOBI, AZW3, FB2DOCX
XLS, ODS, CSV, TSV, NumbersXLSX
PPT, ODP, KeyPPTX

output=formatted (the default) keeps the document's layout, tables, images and styling, and translates text inside images and scanned pages in place. output=text returns the translation as HTML text without the original layout and usually costs fewer credits. Text output needs a document with extractable text; use formatted for scans and photos.

Limits per document: 50 MB, 1,000 PDF pages, and 1,000,000 extracted characters. Up to 5 documents can be in progress per account at once.

Get a free quote

POST /documents/quote prices a document without translating it or charging anything. Send multipart/form-data:

FieldRequiredDescription
fileYesThe document.
tgtYesTarget language code from /languages, for example spa_Latn.
srcNoSource language code, or auto (default) to detect it.
tierNofast (default) or advanced.
outputNoformatted (default) or text.
sh
curl https://www.polytranslator.com/api/v1/documents/quote \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY" \
  -F file=@contract.pdf -F tgt=spa_Latn -F tier=advanced
json
{
  "object": "document_quote",
  "quote_id": "0d4a8c0e-5a6f-4f58-9d7a-1f1f7d6e0a52",
  "credits": 118,
  "balance_credits": 1000,
  "sufficient_credits": true,
  "expires_at": "2026-09-23T18:30:00+00:00",
  "tier": "advanced",
  "output": "formatted",
  "document": {"filename": "contract.pdf", "format": "pdf", "pages": 12, "characters": 31840}
}

A quote is optional. POST /documents prices the file the same way, and a completed document is charged exactly its quoted credits. Pass quote_id to require the price you were shown: if that quote has expired or was made for a different file or options, the request returns 409 quote_expired instead of charging a different amount.

Translate a document

POST /documents accepts the quote fields plus:

FieldRequiredDescription
quote_idNoHold the job to a previously shown quote.
max_creditsNoReject the job with 409 credit_limit_exceeded (before reserving anything) if it would cost more.

Send a fresh UUID in the Idempotency-Key header for each document, and keep it when retrying. A retry with the same UUID, file and options returns the existing document (200) without charging again; the same UUID with a different file or options returns 409 idempotency_conflict.

The response is 202 Accepted with a document object and a Location header. The quoted credits are reserved from your purchased balance immediately. They are charged when the document completes and returned automatically if it fails or is canceled before work starts.

Wait for the result

GET /documents/{id} returns the current document object. Add ?wait=N (0–60 seconds) to hold the request until the document finishes or N seconds pass, so a simple loop needs no sleep:

sh
curl "https://www.polytranslator.com/api/v1/documents/$DOCUMENT_ID?wait=60" \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY"

Poll with wait rather than a tight loop; long scanned books take longer than short files. Documents belong to the account that created them: other accounts' keys receive 404.

Download

  • GET /documents/{id}/file returns the translated file in the format shown in the table above (output=text documents return HTML).
  • GET /documents/{id}/pdf returns a PDF rendition of the translation, useful for previews.

Both return 409 not_ready until status is completed. The files object on a completed document contains both URLs. Keep your own copy: translated files are retained for 30 days.

List and cancel

  • GET /documents?limit=20 lists the account's most recent API documents (up to 100).
  • POST /documents/{id}/cancel stops a document that is still running. Work that has not started is refunded; the response shows the final credits.

The document object

json
{
  "id": "5f0c6f55-2d0b-4d4e-9d7c-3c1f5a0b9e21",
  "object": "document",
  "status": "completed",
  "filename": "contract.pdf",
  "format": "pdf",
  "src": "auto",
  "tgt": "spa_Latn",
  "tier": "advanced",
  "output": "formatted",
  "progress": {"done": 12, "total": 12, "phase": null},
  "credits": {"reserved": 118, "charged": 118, "refunded": 0},
  "created_at": "2026-09-23T18:02:11+00:00",
  "completed_at": "2026-09-23T18:04:40+00:00",
  "error": null,
  "files": {
    "document": "https://www.polytranslator.com/api/v1/documents/5f0c6f55-2d0b-4d4e-9d7c-3c1f5a0b9e21/file",
    "pdf": "https://www.polytranslator.com/api/v1/documents/5f0c6f55-2d0b-4d4e-9d7c-3c1f5a0b9e21/pdf"
  },
  "partial": false
}
StatusMeaning
queuedAccepted and waiting for a worker.
processingBeing read and translated. progress.phase is read or translate for scanned and page-image documents.
completedDone. Download from files.
failedNot translated; error.code says why and reserved credits are returned.
canceledStopped by POST /documents/{id}/cancel.

credits.charged and credits.refunded are null and 0 until the document settles. partial: true means some text could not be translated. Failure codes include unreadable_document (the scan is too blurry or damaged to read), nothing_translated (no translatable text, or already in the target language), timeout, and internal_error; submit again with a new Idempotency-Key after addressing the cause.

Document errors

HTTPCodeAction
402insufficient_creditsBuy credits, then retry with the same Idempotency-Key. Includes required_credits and balance_credits. If automatic refill is enabled it is attempted first.
409credit_limit_exceededThe document costs more than max_credits (required_credits included).
409quote_expiredRequest a new quote or omit quote_id.
409idempotency_conflictUse a new UUID for a different file or options.
413file_too_large, too_many_pages, text_too_longSplit the document.
422unsupported_format, unreadable_document, no_text, unsupported_pairFix the file or options. no_text means use output=formatted.
429concurrent_document_limitFive documents are already running; retry after Retry-After.
503busyRetry after Retry-After seconds with the same Idempotency-Key.

Choose an onboarding path

Agents with an authorized Stripe Shared Payment Token: securely generate and save an API key, then call POST /purchase-key with the key, an email address, a credit-pack ID, a purchase UUID, and the payment token. The endpoint provisions the account and purchases credits. A successful payment makes that same key ready for translation. No Polytranslator browser session is required.

People using a card: sign in to the dashboard, purchase a credit pack, and create an API key. Use the billing controls to save a card and optionally enable capped automatic refill.

An agent can also call POST /accounts before buying credits. Account creation has no charge and grants no API credits. Existing accounts require an existing account credential; knowing an email address or holding a payment token does not grant access to an existing account.

Headless payment requires a payment credential that the cardholder has already authorized. Polytranslator does not issue Stripe Shared Payment Tokens or accept raw card numbers. Obtain a token scoped to our merchant, the pack's amount, and its currency from a compatible payment provider. A token or a bank may require its owner's interaction. A single-purchase token does not authorize recurring charges. See Stripe's Shared Payment Token guide.

Account credits and credentials

Send requests with Authorization: Bearer YOUR_API_KEY.

Purchased credits belong to your account. The website and every API key on that account share the same purchased balance. Buying another pack replenishes that balance; keep using the same API key. A subscription is not required, including for the Advanced tier.

API translations consume only purchased credits. Free daily credits, promotional credits, and monthly subscription allowances are not eligible for API usage. Your website's total displayed balance can therefore be higher than the balance available to your API keys. Check GET /credits for the available API balance.

There are two key permissions:

  • Translation: translate and read the account's purchased balance. This is the default for additional keys created in the dashboard.
  • Manage billing: purchase credits, manage saved-card authorization and automatic refill, and manage API keys. Headless enrollment creates a key with this permission. Grant it only to agents authorized to spend for the account.

Keys with either permission use the same account balance. Automatic refill is an account policy: once enabled, translation activity from any account key can trigger it within the policy's limits.

Keep keys in server-side configuration or a secret store, not browser code or public repositories. Keep payment tokens and Stripe client secrets private as well. Revoke an exposed key from the dashboard. Revoking a key does not delete the account's credits or disable its automatic-refill policy; manage that policy separately.

Discover packs and payment support

GET /catalog is public. Fetch it before authorizing a payment to get packs, currency, payments_available, spt_available, and stripe_profile_id. It also supplies purchase_endpoint and provisioning_endpoint. Use its price and Stripe merchant profile when obtaining a token; do not use an amount supplied by an end user as the purchase price.

Only attempt token purchases when spt_available is true. When it is false, the token purchase endpoints return 503 spt_unavailable; do not treat this as a declined payment or create another account. Card setup and ordinary Checkout availability are reported by payments_available.

sh
curl --fail-with-body https://www.polytranslator.com/api/v1/catalog

Current packs are listed below. The API catalog is authoritative.

Pack IDPurchased creditsPrice in USDprice_cents
credits_300300$9.99999
credits_10001,000$29.992999
credits_20002,000$49.994999
credits_50005,000$99.999999

The server owns pack prices and credit quantities. Purchases are credited only after Stripe confirms successful payment. An account created by an unsuccessful purchase remains available with its original key, so retry or reconcile that purchase without creating another account.

Headless account creation

New accounts use a client-generated enrollment key: pk_live_ followed by 64 lowercase hexadecimal characters from a cryptographically secure random generator. Generate and securely save it before the first request. The server stores only its hash. Saving first makes a lost enrollment response recoverable by repeating the request with the same key and email.

Use a contact email you control. Email verification is not a prerequisite for this purchased-credit flow. If the email already belongs to an account and the supplied key does not authenticate it, the endpoint returns 409 sign_in_required; sign in or use that account's existing key. Do not generate a new account for each refill.

When the owner first verifies the account's email—for example, through a verification link, password recovery, or verified Google/Apple sign-in—credentials created before that ownership check are revoked. Earlier API keys and browser sessions stop working, and saved-card authorization and automatic refill are cleared. Purchased credits stay with the account. After first verification, create replacement API keys in the dashboard and reauthorize the card and refill policy if needed. Keep using the same account; there is no need to buy those credits again.

POST /accounts accepts:

json
{"email":"agent-owner@example.com","name":"Translation agent"}

Send the enrollment key in the Authorization header. name labels the key and is optional (at most 64 characters). A successful response contains:

json
{
  "account_id": "de74c341-807d-4468-a92d-03cc1692beca",
  "email": "agent-owner@example.com",
  "key_id": "9d632c4b-ea84-4aad-bf2f-33c736dcedf9",
  "key_prefix": "pk_live_00000000",
  "key": "YOUR_SAVED_ENROLLMENT_KEY",
  "can_manage_billing": true,
  "funding": "purchased"
}

key echoes the credential you supplied; it is not a server-side recovery mechanism. Retrying with an existing key preserves its permission rather than upgrading it. Creating an account does not purchase credits.

First purchase and refills

POST /purchase-key combines account provisioning and a credit purchase:

http
POST /api/v1/purchase-key
Authorization: Bearer YOUR_SAVED_ENROLLMENT_KEY
Content-Type: application/json
json
{
  "email": "agent-owner@example.com",
  "name": "Translation agent",
  "pack_id": "credits_300",
  "idempotency_key": "1a1e2f51-450b-44b4-b1dd-2df37c68af16",
  "payment_token": "YOUR_AUTHORIZED_STRIPE_SHARED_PAYMENT_TOKEN"
}

The response includes account_id, email, key, key_id, key_prefix, can_manage_billing, balance_credits, and purchase. Retain the same API key for all future calls. Payment can be pending or require action; do not assume an HTTP success means credits were granted. Inspect purchase.status and purchase.credits_granted.

The purchase object contains purchase_id, status, credits, price_cents, currency, and credits_granted. It may also include payment_intent_id, client_secret, action_type, action_url, checkout_url, and failure_reason; these optional fields can be null.

For an existing account, a billing-enabled key can call POST /credit-purchases with:

json
{
  "pack_id": "credits_300",
  "idempotency_key": "fb6b1e4c-91e9-4ad4-9888-82642a823066",
  "payment_token": "YOUR_NEXT_AUTHORIZED_STRIPE_SHARED_PAYMENT_TOKEN"
}

Each intended new purchase needs a new purchase UUID and a new authorized payment token. A refill changes the account balance, not the API key. Returning accounts may also use /purchase-key with their existing key and the same email.

POST /credit-purchases returns the purchase object directly; /purchase-key nests it under purchase alongside account information. Token strings start with spt_; supply the complete authorized token, not a card number or a Stripe PaymentMethod ID. Supply exactly one of payment_token or payment_method: "saved".

Use hosted Checkout instead of a token

If the owner will complete a browser checkout, a billing-enabled key can create it with POST /billing/checkout:

json
{
  "pack_id": "credits_300",
  "idempotency_key": "0ad108b7-c74c-4e88-bbea-f5e1b9d07721",
  "return_url": "https://www.polytranslator.com/api-keys/"
}

The purchase response supplies checkout_url when its status is checkout. Open that URL for payment, then reconcile GET /credit-purchases/{purchase_id}. The return redirect alone does not prove payment. This buys credits for the authenticated account; it does not enroll a card for automatic refill. Card enrollment is described separately below.

Purchase retries and payment states

Persist the purchase UUID and the payment authorization before sending a purchase. Retry an uncertain result with the same purchase UUID, pack, token, and account key. Replaying an already processed payment token for its original purchase returns the existing purchase without charging again. Reusing a UUID or token for a different account, pack, or payment authorization is rejected.

Once you have a purchase_id, call GET /credit-purchases/{purchase_id} with a billing-enabled key to reconcile its current state. If the original response was lost before you obtained that ID, repeat the original purchase request. Do not replace its UUID or submit a new authorization merely because the connection timed out.

Purchase statusMeaning and next step
paidPayment is verified and purchased credits are granted. Translate or retry the unfunded translation.
pendingPurchase is recorded but payment is not yet confirmed. Retry the original request if necessary, and query its status.
checkoutThe owner must complete the returned hosted checkout_url, then query the purchase's status.
processingStripe is still processing the payment. Poll the existing purchase; do not charge again.
requires_actionThe bank or payment provider requires authentication. Follow the returned payment action, then query the same purchase.
failedThe purchase failed. Resolve the payment problem before deliberately creating a new purchase.
expiredThis purchase can no longer start a payment. Reconcile its status before deliberately creating a new purchase.

For requires_action, inspect action_type:

  • shared_payment_token_action: complete authorization with the wallet or provider that issued the Shared Payment Token, then reconcile the original purchase. This action must not be passed to the merchant's Stripe.js authentication flow.
  • Other action types may supply an action_url for the cardholder or require Stripe's client SDK with client_secret. The dashboard's payment-review control handles supported merchant authentication flows. A redirect URL is not guaranteed.

Treat client_secret as a private payment credential, not an API key. An agent should surface required human action instead of repeatedly charging or silently changing authorization. Completing authentication does not justify a new purchase: query the original purchase for its verified outcome.

Python quickstart

This standard-library example saves its API key and purchase UUID in a private file before sending anything. It uses an authorized token from POLYTRANSLATOR_PAYMENT_TOKEN and a contact email from POLYTRANSLATOR_EMAIL. Keep the same token available when rerunning after an uncertain response. The example buys at most its one intended pack; it does not start another purchase on a retry.

python
import json
import os
from pathlib import Path
import secrets
import urllib.error
import urllib.request
import uuid

BASE = "https://www.polytranslator.com/api/v1"
state_path = Path.home() / ".config" / "polytranslator" / "quickstart.json"
state_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
initial_state = {
    "key": "pk_live_" + secrets.token_hex(32),
    "email": os.environ["POLYTRANSLATOR_EMAIL"],
    "purchase_uuid": str(uuid.uuid4()),
    "translation_uuid": str(uuid.uuid4()),
}
try:
    fd = os.open(state_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
except FileExistsError:
    pass
else:
    with os.fdopen(fd, "w") as file:
        json.dump(initial_state, file)
with state_path.open() as file:
    state = json.load(file)

def call(path, body=None, request_id=None):
    headers = {"Authorization": "Bearer " + state["key"]}
    data = None
    if body is not None:
        headers["Content-Type"] = "application/json"
        data = json.dumps(body).encode()
    if request_id is not None:
        headers["Idempotency-Key"] = request_id
    request = urllib.request.Request(BASE + path, data=data, headers=headers)
    try:
        response = urllib.request.urlopen(request, timeout=90)
    except urllib.error.HTTPError as error:
        response = error
    with response:
        return response.status, json.load(response)

# Inspect GET /catalog first and obtain a token for credits_300's current price.
status, result = call("/purchase-key", {
    "email": state["email"],
    "name": "Translation agent",
    "pack_id": "credits_300",
    "idempotency_key": state["purchase_uuid"],
    "payment_token": os.environ["POLYTRANSLATOR_PAYMENT_TOKEN"],
})
if status != 200 or result["purchase"]["status"] != "paid":
    # Inspect privately: results can contain payment credentials.
    raise SystemExit("Purchase needs reconciliation or action; retry the same purchase.")

status, result = call("/translate", {
    "src": "eng_Latn", "tgt": "lat_Latn", "text": "Hello",
    "tier": "fast", "max_credits": 1,
}, request_id=state["translation_uuid"])
if status == 200:
    print(result["text"])
else:
    raise SystemExit(f"Translation returned HTTP {status}; retain the same retry UUID.")

Use your platform's secret store in production. This file is an example of persistent client state, not a substitute for centralized secret management. Do not delete it to recover from a timeout: that would discard the original account credential and retry identities.

List languages

GET /languages is public and requires no API key.

sh
curl https://www.polytranslator.com/api/v1/languages

The response contains a languages array. Each entry has a code, name, and low_resource boolean. Pass the returned language codes as src and tgt; for example, English is eng_Latn and Latin is lat_Latn.

Check purchased credits

sh
curl https://www.polytranslator.com/api/v1/credits \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY"

Example response:

json
{
  "balance_credits": 300,
  "reserved_credits": 0,
  "funding": "purchased",
  "rates": {
    "fast": {"credits": 1, "characters": 1000},
    "advanced": {"credits": 3, "characters": 1000}
  }
}

balance_credits is the purchased balance available for new work. reserved_credits reports purchased credits held for work in progress; those credits are not available to spend again.

Translate text

POST /translate accepts JSON:

FieldRequiredDescription
srcYesSource language code from /languages.
tgtYesTarget language code from /languages.
textYesBetween 1 and 50,000 Unicode characters.
tierNofast (default) or advanced.
max_creditsNoNonnegative integer limiting the credits this request may consume. A request costing more is rejected before translation.

Send a fresh UUID in the Idempotency-Key header for each distinct translation. Keep that UUID when retrying the same request after a timeout or dropped connection.

sh
curl https://www.polytranslator.com/api/v1/translate \
  -H "Authorization: Bearer $POLYTRANSLATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 55dd5d8d-fc8f-44de-acf5-eb83a84d2ffc" \
  -d '{"src":"eng_Latn","tgt":"lat_Latn","text":"Hello","tier":"fast","max_credits":1}'

Example success response:

json
{
  "text": "Salve",
  "usage": {"chars": 5, "credits_charged": 1},
  "request_id": "55dd5d8d-fc8f-44de-acf5-eb83a84d2ffc",
  "credits_remaining": 299
}

usage.chars counts source characters, including whitespace, using Unicode code points rather than bytes. credits_remaining is a snapshot of the available purchased balance after completion. Other requests or website usage can change the balance; use /credits for a fresh snapshot.

Credit costs

Costs depend on the source text length and tier, not the translated output length:

  • Fast: max(1, ceil(characters / 1000)) credits.
  • Advanced: max(3, ceil(characters * 3 / 1000)) credits.
Source charactersFast creditsAdvanced credits
1–1,00013
1,00124
2,00026
50,00050150

Advanced rounds the final credit amount up; it does not round the character count up to a block of 1,000 first. Credits are reserved before translation and charged on success. Failed translations release their reservation. Requests share the same balance, so concurrent requests cannot spend the same credits twice.

There is no subscription requirement or daily character quota. The request-rate limits are 30 requests per minute for POST /translate and 60 requests per minute for GET /credits, each shared by all API keys on an account. Retried requests count toward these limits. Per-request size limits also apply.

Retries and idempotency

Idempotency-Key is optional, but agents and production clients should always supply it. Without a client-supplied key, a retry can become a new translation with a new charge.

Translation responses after request validation echo the UUID in both request_id and the Idempotency-Key response header. If no UUID was supplied, the server generates one. An invalid UUID returns 400 invalid_request_id.

  • Repeating the same request with the same UUID on the same account returns the stored result without another translation or charge. This also works through another API key belonging to that account.
  • If the request is still running, a duplicate returns 202 with Retry-After: 2. Wait, then send the same request and UUID again.
  • Reusing a UUID with different request parameters returns 409. Use a new UUID for a different translation or a changed spending limit.
  • Stored responses expire after seven days. A retry after expiry returns 409 and does not run or charge again. Use a new UUID only if you intend to create a new translation.
  • A terminal translation failure is also stored. Retrying that UUID returns the failure; use a new UUID to start a fresh attempt after resolving the problem.
  • An initial 402 insufficient_credits does not start a translation. After replenishing the account, retry the same request and UUID.

If the calculated cost exceeds max_credits, the API returns 409 credit_limit_exceeded with error.required_credits before reserving credits or starting translation. Choose a spending limit that covers the intended request; use a new UUID when changing request parameters.

Insufficient credits and refills

If the available purchased balance cannot cover a translation, the API returns 402:

json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Purchase credits to translate",
    "required_credits": 3,
    "balance_credits": 1
  },
  "request_id": "55dd5d8d-fc8f-44de-acf5-eb83a84d2ffc"
}

Buy another pack through POST /credit-purchases, or purchase in the dashboard while signed in to the same account. After payment succeeds, retry the translation with the same Idempotency-Key. The existing API key continues to work.

If automatic refill is enabled, the server can purchase the configured pack when the account's purchased balance falls below its threshold, then retry an unfunded translation once. A payment that is still processing, needs authentication, or would exceed the refill budget cannot guarantee an immediate translation. Reconcile the existing purchase and retry the original translation after credits become available.

Saved cards and automatic refill

Saving a card and authorizing automatic refill are separate actions. Both are available in the API dashboard and through the billing API. Billing routes require a billing-enabled API key or the account owner's website session. An explicit invalid API key does not fall back to a browser's signed-in account.

GET /billing returns user_id, balance_credits, reserved_credits, a payment_method summary or null, and the auto_refill policy, together with the catalog fields. Read it before changing the policy; updates require auto_refill.policy_revision as expected_revision to avoid overwriting a newer change. The card summary contains id, brand, and last4; it does not contain card numbers.

The examples below use Bearer authentication. With a browser session cookie instead, every billing mutation must also include expected_user_id set to the user_id just read from /billing. This applies to credit purchases, hosted Checkout, card setup, refill-policy updates, and card removal. A Bearer-authenticated caller may omit this field; if supplied, it must match the key's account. An account mismatch returns 409 purchase_context_changed, so refresh the account context before confirming the action again.

Authorize a card

Start Stripe's hosted card setup with POST /billing/setup:

json
{
  "idempotency_key": "9802cd06-d826-4bd4-a682-305d1826741f",
  "return_url": "https://www.polytranslator.com/api-keys/",
  "consent": true
}

consent: true records explicit authorization to save the card for future off-session credit purchases. Only send it when the account owner has authorized that use. Open the returned checkout_url and complete Stripe's setup. This path can require cardholder interaction; it is not a promise that every new card can be enrolled headlessly.

The response has setup_id, status, and checkout_url. The return URL must be an allowed Polytranslator HTTPS URL (localhost is allowed for development). Stripe redirects back with payment_setup and setup_result query parameters. A successful-looking redirect is not proof of enrollment: query GET /billing/setup/{setup_id} to verify it.

Setup statusMeaning
pendingSetup has been recorded; retry with the same setup UUID if its initial response was lost.
checkoutComplete the returned Stripe Checkout URL, then query this setup again.
completedStripe setup was verified and the card was saved. Automatic refill is still a separate opt-in.
expiredStart a new setup if card enrollment is still desired.
supersededCard or billing settings changed while this setup was open. Start a new setup to apply a new card.

Replacing a saved card disables any existing automatic-refill policy. Reload /billing and explicitly re-enable the policy for the new card if desired; saving the replacement does not carry over permission to start automatic charges.

Enable a capped policy

With an authorized saved card, call PUT /billing/auto-refill:

json
{
  "enabled": true,
  "threshold_credits": 50,
  "pack_id": "credits_300",
  "monthly_limit_cents": 2997,
  "expected_revision": 0,
  "consent": true
}

Replace expected_revision with the current auto_refill.policy_revision from /billing. This example authorizes purchasing 300 credits when the available purchased balance falls below 50, spending at most $29.97 on automatic refills per UTC calendar month. Use the pack's current price from /catalog when choosing a limit. The response to a policy update is the updated policy directly, without the surrounding auto_refill property. Every update that enables a policy requires consent: true.

Automatic refill is off by default. The threshold must be positive and below the selected pack's credit quantity. The monthly limit must cover at least one pack and cannot exceed 1,000,000 cents ($10,000). The monthly budget counts purchases in progress as well as successful purchases, so concurrent translations cannot each start a separate refill or exceed the cap. A failed payment does not make it safe to discard an unresolved purchase: existing payments are reconciled using their original identities.

Choose a threshold that covers your largest expected translation. A request can need more credits than the balance even while the balance remains above your refill threshold; that request returns insufficient credits without overriding the authorized threshold.

Read /billing to inspect the current policy, spending, and refill state. The policy includes monthly_spend_cents (successful and in-flight automatic purchases this UTC month), policy_revision, status, last_error, and an optional active_attempt with id, status, and purchase_id.

Refill policy statusMeaning
disabledAutomatic purchases are off.
readyPolicy is enabled and waiting for its balance threshold.
processingA refill is in progress or its payment outcome is unresolved. Reconcile that purchase; its budget remains reserved.
attentionThe owner needs to resolve a card or payment problem and explicitly re-enable the policy.
budget_limitedAnother pack would exceed this UTC month's automatic-refill budget.
unavailableCredit purchases are currently unavailable.

A background worker also reconciles pending refills. If a bank requires authentication for an automatic charge, the server tries to cancel and verify that unpaid charge before releasing its budget, disables the policy, and reports authentication_required. The owner can enroll an appropriate card and re-enable the policy. An uncertain cancellation remains in progress with its budget reserved until the original payment is reconciled.

To disable automatic refill, send the current policy with enabled: false and the current expected_revision. To remove the authorized card, call POST /billing/payment-method/remove with {"expected_revision": CURRENT_REVISION}. Removing the card disables automatic refill. A policy change cannot undo a payment that has already started; its result will still be reconciled and any successful purchase credited.

Purchase once using an authorized saved card

A billing-enabled key can buy a pack with POST /credit-purchases:

json
{
  "pack_id": "credits_300",
  "idempotency_key": "3b7dcfa6-46ef-4ebf-b51d-bb50e5cf794d",
  "payment_method": "saved"
}

This is an explicit purchase by a caller with spending authority. Use only after the owner has authorized the saved-card purchase. Do not treat an automatic-refill budget as permission for unlimited separately requested purchases. As with token purchases, retain the same UUID on retries and inspect the payment state before spending the resulting credits.

GET /billing/purchases returns the account's most recent 50 purchases, including created_at and pack_id. This is a history view; reconcile a pending purchase with its status endpoint rather than assuming the history snapshot has its latest Stripe state.

Manage additional API keys

The signed-in account owner or a billing-enabled key can manage keys:

Method and pathRequest / result
GET /api-keysReturns keys with id, prefix, name, can_manage_billing, created_at, and last_used_at. Never returns stored secrets.
POST /api-keysAccepts {"name":"Translation worker","can_manage_billing":false}. Returns the new key's metadata and its secret in key; save it immediately.
DELETE /api-keys/{key_id}Revokes an account key and returns {"revoked":true}.

An account may have two active keys. The list endpoint is limited to 60 requests per minute per account; creation and revocation are limited to 10 per minute per account. Headless account enrollment is limited to 10 requests per minute per IP address.

Creating a third active key returns 409 too_many_keys. Revoke an unused key before creating a replacement. If a server-generated key's creation response is lost, list the account's keys, revoke the unreceived key, and create a replacement; key-list responses cannot recover its secret.

can_manage_billing defaults to false for additional keys. Use a translation-only key for workers and retain a billing-enabled key for provisioning and payments. Granting billing permission also grants key-management authority. A translation-only key attempting a billing or key-management operation receives 403 billing_scope_required.

When creating a billing-enabled key using a browser session cookie, include expected_user_id from the current account. It is optional for a Bearer-authenticated owner, and for translation-only key creation; whenever supplied, it must match the authenticated account or the request returns 409 purchase_context_changed.

Response handling

HTTP statusClient action
200Read the translation or requested resource.
202The translation with this UUID is pending. Retry the same request after Retry-After.
400Read the error code and correct the request; for example, translation Idempotency-Key must be a UUID.
401Supply a valid, active API key.
403Use a billing-enabled key for billing or key management.
402Read the error code and credit amounts; replenish purchased credits when the code is insufficient_credits.
409Read the error code: credit_limit_exceeded means the request exceeds max_credits; idempotency_conflict means the UUID was already used with different parameters; idempotency_expired means the stored response has expired.
413Split text into requests of at most 50,000 characters, each with its own UUID.
422Correct the request fields or language codes.
429Back off and retry with the same UUID.
5xxRetry an uncertain outcome with the same UUID first. If that UUID returns a stored terminal failure, start a new attempt with a new UUID.

Application errors use an error object with code and message; request-validation and rate-limit responses may use the framework's error format. Do not assume every unsuccessful response has the same JSON shape.

Billing and enrollment add these error codes:

CodeClient action
invalid_enrollment_key (400)For a new account, generate and save a cryptographically random key in the documented format.
sign_in_required (409)Use the existing account's credential or sign in; the supplied key cannot claim that email.
billing_scope_required (403)Use a key with billing permission or the owner's website session.
consent_required (400)Obtain authorization before saving a card; do not assert consent automatically.
invalid_return_url (400)Use the allowed Polytranslator dashboard return URL.
payment_conflict (409)Compare with the original purchase/setup request. Do not change its retry identity to escape the conflict.
purchase_context_changed (409)Reload the signed-in account and billing state before confirming again. Cookie-authenticated billing mutations require a matching expected_user_id.
payment_method_required (409)Complete saved-card authorization before selecting the saved payment source.
policy_changed (409)Reload /billing, review the latest settings, and use the current revision.
invalid_refill_policy (400)Correct the policy using the returned message; enabled policies need an authorized saved card and explicit consent.
not_found (404)Confirm the purchase/setup ID belongs to the authenticated account.
spt_unavailable / payments_unavailable (503)Check /catalog. Wait or use an available payment route; retain any existing purchase identity.
payment_pending (503)Payment outcome is uncertain. Reconcile or retry the original purchase; do not send a new charge.

Credit purchases, card setup, and combined provisioning are limited to 10 requests per minute (combined provisioning by IP; authenticated purchases/setup by account). Purchase/setup status and billing snapshots allow 60 requests per minute per account. Retry polling with backoff.

Ready to translate your first document?

Create a key and buy credits in the dashboard. Quotes are free.

Get an API key