LocalizationInternationalizationapi

Localize over the API: strings, email, and documents, end to end

Get from an API key to translated output in five calls, then the rest of the loop: string catalogs with server-side deltas, email templates by webhook, DOCX round-trips, glossaries and translation memory, and results in any format.

Vitalii Vlasiuk
Vitalii Vlasiuk11 min read
On this page

Everything the editor does, a script can do: import, translate across languages with your glossary and translation memory, review, export. This guide takes you from a key to translated output, then through each part of the loop.

The interactive reference and OpenAPI JSON are the exhaustive contract; transept.ai/developers is the one-page version of this guide.

One naming quirk before the first call: the billing unit is words everywhere a person reads, but API response fields keep the historical name credits (estimatedCredits, spendable). Same quantity.

Get a key

Settings → Developer in the app; copy the secret once. Requests send it as Authorization: Bearer tsk_live_…, header only, never in a URL.

KeyMinted byBilled toUse for
Personal keyYou, in Settings → DeveloperYour word balanceScripts, CI, your own automations
Team keyA team owner, in team settingsThe team word poolShared pipelines that survive a person leaving

Keys are scoped: full access, or read/write per area (documents, runs, glossaries, style guides, translation memory, webhooks, projects). A call outside the key's scopes gets a 403 naming what's missing. A key can never mint another key; key management stays in the app.

The loop in five calls

Prove the key works, create a document, price the run, start it, read it back.

export KEY="tsk_live_…"
BASE="https://app.transept.ai/api/public/v1"

# 1: who am I, what can this key do, what's the balance?
curl -s $BASE/me -H "Authorization: Bearer $KEY"
# 2: a document plus its target-language fan-out, in one call
curl -s -X POST $BASE/documents \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title": "Welcome email", "content": "# Welcome\n\nStart your free trial.",
       "content_type": "markdown", "source_language": "en",
       "target_language": "de", "target_languages": ["fr", "uk"]}'
# → { document_id, language_group: [ {id, target_language}, … ] }
# 3: price it (free, no side effects), then run it across every language
curl -s $BASE/workflow-templates -H "Authorization: Bearer $KEY"   # discover ids

curl -s -X POST $BASE/group-runs/estimate \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"document_id": "<master>", "template_id": "end_to_end", "target_languages": "all"}'
# → { lanes: [ {language, estimatedCredits, minCredits} ], totals: {sufficient} }

curl -s -X POST $BASE/group-runs \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-2026-08-05" \
  -d '{"document_id": "<master>", "template_id": "end_to_end", "target_languages": "all"}'
# → { id: <group_run_id>, runs: [ {job_id, language, block_count} ] }
# 4: wait by polling, or get pushed a signed event instead
curl -s $BASE/group-runs/<group_run_id> -H "Authorization: Bearer $KEY"
# → { status, runs: [ {language, status, progress, gate} ] }
# 5: read the results in the format your pipeline wants
curl -s "$BASE/documents/<version-id>/content?format=markdown" \
  -H "Authorization: Bearer $KEY"

Three habits from day one: estimate first (free; the range is floor to ceiling, and the ceiling settles back to actual spend), send an Idempotency-Key on job-creating POSTs (a retry replays the first result instead of billing twice), and register a webhook (POST /webhooks {url, events: ["group_run.completed"]}) instead of polling. Deliveries are HMAC-signed with retries and a delivery log.

Three shapes of input

You have…Send it asEndpoint
Prose: an email body, an article, a pageInline markdown / HTML / textPOST /documents, or a payload webhook
A file: DOCX, HTML, CSV, PO, XLIFFMultipart uploadPOST /documents/upload
UI text or game strings with keysA string catalog (.tstrings.json)POST /documents/import-strings

Prose and email

POST /documents with content_type: "html" keeps formatting, links, and template placeholders through translation; format=html returns the translated email in the same shape. Or skip the request: set a workflow trigger to "the request body is the content" and point your ESP, Zapier/n8n, or CI at the trigger URL:

# the trigger URL (with its secret) comes from the workflow's trigger card;
# the secret IS the auth, so no Authorization header
curl -s -X POST $BASE/hooks/<trigger-secret> \
  -H "Content-Type: application/json" \
  -d '{"content": "<h1>Welcome!</h1><p>Your trial starts today.</p>",
       "content_type": "html",
       "title": "Trial welcome",
       "external_id": "welcome-v4"}'
# → 202 { run id, document_id, created }

# raw bodies work too: POST the markdown/HTML itself with its Content-Type
curl -s -X POST $BASE/hooks/<trigger-secret> \
  -H "Content-Type: text/markdown" \
  --data-binary $'# Welcome\n\nStart your free trial.'

The trigger decides whether each delivery creates a fresh document or updates one fixed document (an update re-aligns content so only changed blocks re-translate). Repeat deliveries dedupe on external_id, or the content hash, for 24 hours.

See webhook triggers.

Files

POST /documents/upload (multipart) with the file plus source_language, target_language, optional target_languages[] and project_id. DOCX round-trips: export reinjects translations into your original file, so styling and layout survive. CSV, PO, and XLIFF import as keyed units and export back in their own format.

Format details and column mapping: strings files.

String catalogs

Each string is a unit with a stable id, a source, and optional context, max_length, and placeholders:

curl -s -X POST $BASE/documents/import-strings \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"document": {
        "format": "transept-strings", "version": 1, "source_locale": "en",
        "target_locales": ["de", "fr", "uk"],
        "metadata": {"name": "App UI"},
        "units": [
          {"id": "app.save", "source": "Save", "context": "Toolbar button"},
          {"id": "app.greeting", "source": "Welcome, {name}", "context": "Dashboard header"}
        ]}}'
# → { processing_job_id }; poll GET /document-jobs/{id}

Context reaches the model as translator guidance, max_length is enforced with shorten-and-retry, placeholders are protected, and plurals expand per language. Translations the file already carries seed as active for free and feed translation memory. Missing target languages fail the call with no_target_languages rather than importing a document that translates into nothing.

The full loop is Localize a string catalog over the API.

One email template, three languages

The email case, concretely. Liquid stays intact through translation:

# the template body goes in as HTML; three target languages fan out
curl -s -X POST $BASE/documents \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"title": "Trial welcome", "content_type": "html", "source_language": "en",
       "target_languages": ["de", "fr", "uk"],
       "content": "<h1>Welcome, {{ first_name | default:\"friend\" }}!</h1><p>Your trial starts today.</p>"}'

{{ first_name | … }} is masked before the model sees the text and restored verbatim, with one exception: the fallback friend is reader-facing copy, so it IS translated while the tag around it is not. Run the group as in the quickstart, then read each language back:

curl -s "$BASE/documents/<de-version-id>/content?format=html" \
  -H "Authorization: Bearer $KEY"
# → the same HTML with German copy, Liquid intact; paste it into the ESP's de variant

Zero-touch variant: a payload webhook trigger takes the template straight from the ESP, the workflow runs on arrival, and a group_run.completed webhook hands the translated HTML back.

Platform specifics (Braze tags, Mailchimp conditionals): ESP email templates and the email localization guide.

Glossaries, style guides, memory

Create a glossary, seeding terms in the same call:

curl -s -X POST $BASE/glossaries \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"name": "Product terms", "source_language": "en",
       "terms": [{"source_term": "Transept", "target_term": "Transept",
                  "do_not_translate": true}]}'

Grow an existing one (GET /glossaries lists what your key can see):

curl -s -X POST $BASE/glossaries/<id>/terms/bulk \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"terms": [
        {"source_term": "workspace", "target_term": "Arbeitsbereich"},
        {"source_term": "run", "target_term": "Lauf", "notes": "the noun: a workflow execution"}
      ]}'

Or generate from a document you trust: POST /glossaries/{id}/auto-build and POST /styleguides/generate, each with a free …/estimate twin. Both bill words and both require auto_apply: true over the API; headless has no review step, so the result commits on completion. Poll GET /generation-jobs/{id}.

The reviewed-in-app alternative: build from a document.

Attach once; every later run uses them, from the API, MCP, or the editor, at no extra cost:

curl -s -X PATCH $BASE/documents/<id>/translation-settings \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"glossary_ids": ["<glossary_id>"], "styleguide_version_ids": ["<active_version_id>"]}'

Style guides attach by active version id (from GET /styleguides/{id}); [] detaches; both also work on POST /documents at create time.

UI side: glossaries, style guides.

Translation memory needs no attaching: every completed translation is indexed and reused automatically. Query it (POST /translation-memory/query, free), seed it from a TMX/XLIFF (POST /translation-memory/import, free), export it all (GET /translation-memory/export-tmx).

See translation memory and bring existing translations.

Teams

A team key bills the team pool and inherits the team's projects, glossaries, style guides, and translation memory. Create documents in the team by passing a team project's project_id (GET /projects; team projects carry a team_id). Everything else is identical to personal use.

Teams and projects are set up in the app: projects, teams and invites.

Runs and review gates

POST /runs works one language version; POST /group-runs fans across every target language, creating missing versions as needed. Both estimate free, take Idempotency-Key, and cancel.

A workflow step set to wait for review parks the run: GET /runs/{id} reports gate_pending: true with a digest of what the review found.

The human side of the pause is the review panel and guided review; the script side:

# continue downstream steps (all blocks, or a reviewed subset):
curl -s -X POST $BASE/runs/<id>/gate/approve \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"approved_block_ids": ["…"]}'    # omit the field to approve all

# a gate with nothing downstream is dismissed instead:
curl -s -X POST $BASE/runs/<id>/gate/acknowledge -H "Authorization: Bearer $KEY"

A gate never continues on its own: something has to call approve, whether that is your script, an assistant over MCP, or a person in the app. There is no run-start flag that pre-approves gates; if nothing should pause at all, use a workflow without a review step.

Output formats

GET /documents/{id}/content?format=<f> on a language-version id:

format=Returns
jsonPer-block source + active translation (default)
stringsThe whole catalog, every language keyed by unit id
markdown / htmlRendered text (html for HTML-born documents)
sourceThe faithful export in the document's birth format
docx / pdfWord file with translations reinjected, or rendered PDF
xliff / tmx / csvCAT-tool, TM, and spreadsheet interchange

format=strings on the master document returns every language in one call, ready to commit.

Update only the delta

For string catalogs, re-submit the whole current file; the server owns the diff:

# 1: re-import the complete current corpus; Transept diffs it by unit id
curl -s -X POST $BASE/documents/<master>/import-strings \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d @app-ui.tstrings.json > delta.json
# → { units: {added, changed, unchanged, removed}, delta_block_ids: {<doc_id>: [block ids]} }

# 2: feed delta_block_ids VERBATIM into a group run; only those blocks translate
jq -n --slurpfile d delta.json \
  '{document_id: $d[0].master_document_id, template_id: "end_to_end",
    target_languages: "all", block_ids: $d[0].delta_block_ids}' \
| curl -s -X POST $BASE/group-runs \
    -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" --data-binary @-

Unchanged strings keep their translations and their review state; fixing one string costs one string. (Pass run: {template_id} inline on the re-import to dispatch the delta in the same call.)

This is the API side of Run only what changed.

For prose documents whose source changed, POST /documents/{id}/resync-source re-aligns blocks: unchanged blocks keep their translations, changed ones go stale, and an only-updated run touches just those. Resync re-parses the stored source file, so edit sources through the import path, not by patching blocks.

AI assistants (MCP)

The same loop as agent tools, at https://app.transept.ai/api/public/v1/mcp: me_getdocument_creategroup_run_estimate / group_run_startgroup_run_getdocument_content_get. Connecting is OAuth (browser consent, no key to paste; a Bearer key works for headless). Tools respect the granted permissions, and anything that spends words answers with the estimate first; the assistant must confirm explicitly.

Per-client setup: Connect your AI assistant.

FAQ

Do estimates, re-imports, or seeded translations cost words?

No. Estimates are free and side-effect-free, re-importing a catalog is free (the diff is computed server-side), and translations your files already carry are seeded as finished work at no cost. Words are spent only when Transept translates something for you. Upper-bound estimates settle down to actual spend, releasing the unused reserve.

Can automation skip my review gates?

No. A workflow step set to wait for review parks the run until something approves it: your script calling the gate-approve endpoint, or a person in the app. If a pipeline must run unattended end to end, configure its workflow without a review step; there is no flag that bypasses a gate you put there.

Is the API available on the Free plan?

Yes. API keys are available on every plan, including Free; there is no separate gate. Your word balance is what limits usage, exactly as in the editor, and the Free tier's monthly words work over the API the same as anywhere else.

How do I localize email templates without exporting them by hand?

Point your email platform at a payload webhook trigger: the POSTed template body becomes the document, the workflow translates it, and the export returns the same HTML shape with translations in place. Placeholders and template syntax are protected through the round-trip. The platform-by-platform mechanics are in the email localization guide.

Where is the exhaustive endpoint reference?

The interactive Swagger reference, generated from the same OpenAPI spec you can import into n8n, Zapier, or a code generator (raw JSON). This guide is the narrative on-ramp; the spec is the contract.

The author

Vitalii Vlasiuk
Vitalii VlasiukCo-founder

Co-founder of Transept, writing as “Mevkh.” A Language and Literature degree, then a turn into software: senior AI engineer shipping production LLM features to 50,000+ users — RAG, agentic tools, LLM-as-judge evaluation. A novelist on the slow path, with 120,000 words of satirical romance fantasy in a drawer. The friction between AI translation and his own prose is what set this whole thing in motion.