DeepL API Authentication with Example Code
DeepL does not use a Bearer token, and the free and pro plans live on different hosts. Working auth code, plus glossaries, formality and the round-trip problem.
Two things trip people up in the first five minutes with DeepL, and both produce an error that looks like your key is wrong when it is not.
First, DeepL does not use Authorization: Bearer <key>. It uses its own scheme: Authorization: DeepL-Auth-Key <key>. Every model and every autocomplete will write Bearer for you, because Bearer is what almost everything else uses.
Second, free and paid keys live on different hosts. Free keys go to api-free.deepl.com and paid keys go to api.deepl.com. A valid free key against the pro host fails, and the error does not say "wrong host."
Here is the working version.
The raw call, so you can see the shape
# Pro key -> api.deepl.com
# Free key -> api-free.deepl.com (the suffix :fx on a key means free)
curl -X POST 'https://api.deepl.com/v2/translate' \
-H 'Authorization: DeepL-Auth-Key YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{
"text": ["Your refund has been processed."],
"target_lang": "DE",
"source_lang": "EN",
"formality": "prefer_more"
}'Note that text is an array, not a string. Each entry is translated independently and comes back in the same order, so you can batch a whole UI's worth of strings in one call. The request size cap is 128 KiB, which sounds generous until an agent decides to translate an entire document in one go.
The parameters that actually matter
- formality: takes
default,more,less,prefer_moreorprefer_less. Here is the trap: asking formoreon a target language that does not support formality is an error, whileprefer_moresilently falls back to default. For an agent translating into many languages, always use theprefer_variants, or you will get failures that depend on which language a customer happens to use. - glossary_id: applies your term list, and it has conditions. It requires
source_langto be set, the glossary's language pair must match the request, and it cannot be combined withglossary_ids. So if you let the agent omit the source language and rely on detection, your glossary quietly stops applying. - context: text that influences the translation without being translated itself, and its characters are not billed. Underused and genuinely helpful. Pass the surrounding sentence, or "this is a button label," and short strings get much better.
- tag_handling:
xmlorhtml, withtag_handling_versionselecting v1 or v2. Use it any time you are translating marked-up content, unless you enjoy repairing broken tags. - model_type:
quality_optimized,prefer_quality_optimizedorlatency_optimized. If a human is waiting on the response, latency matters; for a batch job it does not.
The error codes you will see
DeepL has two that are easy to confuse. 429 and 529 both mean too many requests, so back off and retry. 456 means your character quota is exhausted, and retrying will never fix it. An agent that treats every failure as retryable will sit in a loop burning time against a 456 that needs a billing decision, not a retry.
Every response carries an X-Trace-ID header. Log it. When you open a support ticket it is the first thing that will be asked for.
Wiring it to an agent without putting the key in your code
The example above is fine for a curl test and wrong for an agent, because the key is sitting in a shell history and will end up in a log, a trace or a model's context. Here is the version where it does not.
npx swytchcode
swy login
swy init --mode=sandbox
swy get deepl
swy auth connect deepl # prompts you to paste the key, once
swy auth status # shows it is connected, never the value
swy discover "translate text" --project deepl
swy list methods deepl
swy info <canonical_id>
swy add method <canonical_id> # only translation; no glossary deletesThere is no .env file and no DEEPL_AUTH_KEY variable in your project. The key goes into a local store outside the project folder and is injected into the request when the call runs, which also means the right DeepL-Auth-Key header format is handled for you and your agent cannot write Bearer by mistake. Details in the auth docs and the reasoning in keep API keys out of the LLM.
From your own code it is one function call. No key handling, no retry loop, no header to get wrong:
import { exec } from "@swytchcode/runtime";
// canonical_id comes from `swy list methods deepl`
const res = await exec("<deepl translate canonical_id>", {
body: {
text: ["Your refund has been processed."],
target_lang: "DE",
source_lang: "EN", // required if you want the glossary to apply
formality: "prefer_more", // prefer_* so unsupported languages don't error
glossary_id: "<your-glossary-id>",
context: "Customer support email, formal tone"
}
});
console.log(res.translations[0].text);The SDK is a thin wrapper over the same kernel the CLI uses, so the allowlist and policies apply identically. Language-specific notes are in the JavaScript runtime docs, with Python and Go available too.
Inside Claude Code or Cursor
Translation is a genuinely nice MCP tool to have in an editor: you are writing copy, you want the German version, you do not want to leave. Connect through the MCP server and the same two allowed methods are all the agent can reach. Setup per client is in the MCP guide, the reference in the MCP docs, and if you want a coding agent to wire this up itself, point it at skills.md.
Policies for a translation API, which sounds unnecessary until it is not
Translation is read-shaped, so the risk is not destruction, it is cost and data. Two rules worth having:
{
"id": "cap-bulk-translation",
"target": ["<deepl translate canonical_id>"],
"when": { "field": "text", "operator": "length>", "value": 50 },
"action": {
"type": "REQUIRES_APPROVAL",
"message": "Agent wants to translate more than 50 strings in one call"
},
"approval_timeout": "2h"
}The point is to catch the loop that decides to translate your entire knowledge base at three in the morning. Check the policy reference for the operators available on your version, and keep the file in version control so the rule has a review history.
The other consideration is what you are sending. Customer messages going to a translation provider is a data-transfer question your privacy review will care about, and the audit log of exactly which strings were sent is the thing that answers it.
Retries, and why translation is almost but not quite idempotent
Translating the same string twice gives you the same output and no side effect, so a retry is safe in terms of data. It is not free in terms of billing, because DeepL bills on characters and a retried batch is a second charge. So you do want backoff on 429 and 529, and you do not want a tight retry loop on a large batch.
Rate-limit errors are retried with exponential backoff automatically. For the big batches, cache by source string plus target language so a repeated translation is a cache hit rather than a billable call. More on the general shape in how Swytchcode retries failed API calls.
The round-trip problem nobody warns you about
If your agent translates a field, stores it, and later translates it back to compare, do not treat the result as the original. Translation is lossy in both directions, and round-tripping a status value, a product name or anything with a glossary term in it will drift. We have watched an agent decide a record had changed because the round trip produced a slightly different phrase.
Keep the source text as the source of truth, store translations alongside it, and never compare a translation against a re-translation to detect change.
What you can build
- Multilingual support replies. Detect the customer's language, draft the answer in English, translate with a glossary so product names survive, and have a human send it. Pairs with an Intercom or FreshDesk agent.
- Docs localisation that respects markup. Pull a page, translate with
tag_handlingset, write the result into Notion or your CMS as a draft. - Inbound triage in any language. Translate incoming messages to one working language so your routing and classification logic only has to handle English, then keep the original for the reply.
- Glossary enforcement as a check. Translate marketing copy, then flag where the glossary term did not appear in the output, which usually means someone phrased the source in a way the glossary could not match.
FAQ
How do I tell a free key from a pro key?
Free keys carry a :fx suffix. If you are seeing auth failures with a key you are certain is correct, check the host before anything else.
Why did my glossary stop working?
Almost always because source_lang was omitted and language detection took over, or because the glossary's language pair does not match the request. Both fail quietly in the sense that you get a perfectly good translation with none of your terms applied.
Should the agent choose the formality level?
It can, and it is one of the few places where letting the model decide works well, since it has the conversation context. Just pin it to the prefer_ variants so an unsupported language degrades instead of erroring.
Can I use this with a private or self-hosted translation service instead?
Yes. Any REST API with an OpenAPI spec or a Postman collection can be added as a custom integration and gets the same allowlist, policy, retry and audit treatment, which is the usual route for teams that cannot send text to a third party at all.
Related reading
More content
How to Debug an AI Agent That Called the Wrong API
Working backwards from the response your agent got to the tool-selection decision that caused it, which is where most of the real debugging time goes.
iPaaS Was Built for Integrations, Not Agents
Deterministic trigger-action versus non-deterministic tool choice, and why the governance model that works for one doesn't transfer cleanly to the other.
Using the Airtable API from an AI Agent
Someone renames a field in the UI and your agent breaks with an error saying the field does not exist, while you are looking right at it. Airtable's schema drift problem, solved.
