# Polytranslator API

Create an account, buy prepaid credits through Stripe, and translate text using an API key. Refill the account to keep using the same key. No subscription is required.

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

This API supports text translation. Document and image translation are available on the website; they do not have public API endpoints in this version.

- [Developer overview](https://www.polytranslator.com/developers/)
- [API keys and billing dashboard](https://www.polytranslator.com/api-keys/)
- [OpenAPI schema](https://www.polytranslator.com/api/openapi.json)
- [Agent discovery](https://www.polytranslator.com/llms.txt)

## 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](https://www.polytranslator.com/api-keys/), 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](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens?agent-seller=agent).

## 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 ID | Purchased credits | Price in USD | `price_cents` |
| --- | ---: | ---: | ---: |
| `credits_300` | 300 | $9.99 | 999 |
| `credits_1000` | 1,000 | $29.99 | 2999 |
| `credits_2000` | 2,000 | $49.99 | 4999 |

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 status | Meaning and next step |
| --- | --- |
| `paid` | Payment is verified and purchased credits are granted. Translate or retry the unfunded translation. |
| `pending` | Purchase is recorded but payment is not yet confirmed. Retry the original request if necessary, and query its status. |
| `checkout` | The owner must complete the returned hosted `checkout_url`, then query the purchase's status. |
| `processing` | Stripe is still processing the payment. Poll the existing purchase; do not charge again. |
| `requires_action` | The bank or payment provider requires authentication. Follow the returned payment action, then query the same purchase. |
| `failed` | The purchase failed. Resolve the payment problem before deliberately creating a new purchase. |
| `expired` | This 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:

| Field | Required | Description |
| --- | --- | --- |
| `src` | Yes | Source language code from `/languages`. |
| `tgt` | Yes | Target language code from `/languages`. |
| `text` | Yes | Between 1 and 50,000 Unicode characters. |
| `tier` | No | `fast` (default) or `advanced`. |
| `max_credits` | No | Nonnegative 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 characters | Fast credits | Advanced credits |
| --- | ---: | ---: |
| 1–1,000 | 1 | 3 |
| 1,001 | 2 | 4 |
| 2,000 | 2 | 6 |
| 50,000 | 50 | 150 |

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](https://www.polytranslator.com/api-keys/) 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 status | Meaning |
| --- | --- |
| `pending` | Setup has been recorded; retry with the same setup UUID if its initial response was lost. |
| `checkout` | Complete the returned Stripe Checkout URL, then query this setup again. |
| `completed` | Stripe setup was verified and the card was saved. Automatic refill is still a separate opt-in. |
| `expired` | Start a new setup if card enrollment is still desired. |
| `superseded` | Card 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 status | Meaning |
| --- | --- |
| `disabled` | Automatic purchases are off. |
| `ready` | Policy is enabled and waiting for its balance threshold. |
| `processing` | A refill is in progress or its payment outcome is unresolved. Reconcile that purchase; its budget remains reserved. |
| `attention` | The owner needs to resolve a card or payment problem and explicitly re-enable the policy. |
| `budget_limited` | Another pack would exceed this UTC month's automatic-refill budget. |
| `unavailable` | Credit 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 path | Request / result |
| --- | --- |
| `GET /api-keys` | Returns `keys` with `id`, `prefix`, `name`, `can_manage_billing`, `created_at`, and `last_used_at`. Never returns stored secrets. |
| `POST /api-keys` | Accepts `{"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 status | Client action |
| --- | --- |
| `200` | Read the translation or requested resource. |
| `202` | The translation with this UUID is pending. Retry the same request after `Retry-After`. |
| `400` | Read the error code and correct the request; for example, translation `Idempotency-Key` must be a UUID. |
| `401` | Supply a valid, active API key. |
| `403` | Use a billing-enabled key for billing or key management. |
| `402` | Read the error code and credit amounts; replenish purchased credits when the code is `insufficient_credits`. |
| `409` | Read 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. |
| `413` | Split text into requests of at most 50,000 characters, each with its own UUID. |
| `422` | Correct the request fields or language codes. |
| `429` | Back off and retry with the same UUID. |
| `5xx` | Retry 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:

| Code | Client 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.
