SubtitleEdit/subtitleedit · error · Exception
Forbidden! {resultContent}
Error message
Forbidden!
{resultContent} What it means
Thrown by the official DeepL API translator (DeepLTranslate) when the POST to /v2/translate returns HTTP 403 Forbidden after all retries are exhausted. A 403 from DeepL means the API key is invalid, the key belongs to a discontinued plan, or the key is for a free tier being used against the pro endpoint (or vice versa).
Source
Thrown at src/libuilogic/AutoTranslate/DeepLTranslate.cs:180
var resultContent = string.Empty;
for (var attempt = 0; attempt <= retryDelays.Length; attempt++)
{
var postContent = MakeContent(text, sourceLanguageCode, targetLanguageCode);
result = await _httpClient.PostAsync("/v2/translate", postContent, cancellationToken);
resultContent = await result.Content.ReadAsStringAsync(cancellationToken);
if (!ShouldRetry(result, resultContent) || attempt == retryDelays.Length)
{
break;
}
await Task.Delay(retryDelays[attempt], cancellationToken);
}
if (result.StatusCode == HttpStatusCode.Forbidden)
{
Error = resultContent;
throw new Exception("Forbidden! " + Environment.NewLine + Environment.NewLine + resultContent);
}
if (!result.IsSuccessStatusCode)
{
Error = resultContent;
SeLogger.Error("DeepLTranslate error: " + resultContent);
throw new Exception($"DeepL failed with status code {(int)result.StatusCode} ({result.StatusCode})" + Environment.NewLine + Environment.NewLine + resultContent);
}
try
{
var resultList = new List<string>();
var parser = new JsonParser();
var x = (Dictionary<string, object>)parser.Parse(resultContent);
foreach (var k in x.Keys)
{
if (x[k] is List<object> mainList)
{View on GitHub (pinned to 17a9f07487)
Solutions
- Regenerate the DeepL API key in the DeepL account and paste it fresh into Settings.
- Match the endpoint to the key type: free keys use api-free.deepl.com, pro keys use api.deepl.com.
- Check the DeepL account dashboard for an active subscription and quota.
- Trim whitespace from the key when storing it in configuration.
Example fix
// before
if (result.StatusCode == HttpStatusCode.Forbidden)
{
Error = resultContent;
throw new Exception("Forbidden! " + Environment.NewLine + Environment.NewLine + resultContent);
}
// after - distinguish auth-plan mismatch from revoked key using DeepL error JSON
if (result.StatusCode == HttpStatusCode.Forbidden)
{
Error = resultContent;
var hint = resultContent.Contains("\"message\":\"Quota exceeded", StringComparison.Ordinal)
? "DeepL quota exhausted - check your plan usage."
: "DeepL rejected the API key - verify it matches this endpoint (free vs pro).";
throw new Exception($"DeepL Forbidden: {hint}" + Environment.NewLine + Environment.NewLine + resultContent);
} Defensive patterns
Strategy: validation
Validate before calling
// Before calling Translate, validate the DeepL key/endpoint pairing
var key = Configuration.Settings.Tools.DeepLApiKey;
var endpoint = Configuration.Settings.Tools.DeepLUrl ?? string.Empty;
if (string.IsNullOrWhiteSpace(key))
throw new InvalidOperationException("DeepL API key is not set.");
var isFreeKey = key.EndsWith(":fx", StringComparison.Ordinal);
var isFreeEndpoint = endpoint.Contains("api-free.deepl.com", StringComparison.OrdinalIgnoreCase);
if (isFreeKey != isFreeEndpoint)
throw new InvalidOperationException($"DeepL key/endpoint mismatch: free keys must use api-free.deepl.com, pro keys must use api.deepl.com. Endpoint: {endpoint}"); Type guard
public static bool IsDeepLConfigured(string key, string endpoint)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(endpoint)) return false;
var isFreeKey = key.EndsWith(":fx");
var isFreeEndpoint = endpoint.Contains("api-free.deepl.com");
return isFreeKey == isFreeEndpoint;
} Try / catch
try
{
return await deeLTranslate.Translate(text, src, tgt, token);
}
catch (Exception ex) when (ex.Message.StartsWith("Forbidden!", StringComparison.Ordinal))
{
// 403 from DeepL: prompt the user to re-check key + endpoint pairing
throw new InvalidOperationException("DeepL returned 403 - verify the API key matches the endpoint (free vs pro).", ex);
} Prevention
- Store the key with the correct endpoint pairing at config time (free key -> api-free.deepl.com).
- Trim the key on save to avoid whitespace-induced 403s.
- Do a one-request health check (e.g. /v2/usage) on Initialize to fail fast with a clear cause.
- Never log the full key; log only its suffix to confirm the right one is loaded.
When it happens
Trigger: Translate() posts to https://api-free.deepl.com or https://api.deepl.com /v2/translate with the configured DeepL API key; the response status is HttpStatusCode.Forbidden and ShouldRetry returned false (403 is not in the retry set) or retries were used up.
Common situations: Free-tier key used against the pro endpoint URL (or reverse); key revoked/expired in the DeepL account; account subscription lapsed; key copied with stray whitespace; wrong regional endpoint.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- DeepL failed with status code {(int)result.StatusCode} ({res
- API key invalid (or perhaps billing is not enabled)?
- "Perhaps billing is not enabled (or API key is invalid)?"
- API key invalid (or perhaps API/billing is not enabled)?
- Perhaps billing is not enabled (or API not enabled or API ke
AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13).
Data as JSON: /api/errors/f1c43e265fb406df.
Report an issue: GitHub.