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

  1. Regenerate the DeepL API key in the DeepL account and paste it fresh into Settings.
  2. Match the endpoint to the key type: free keys use api-free.deepl.com, pro keys use api.deepl.com.
  3. Check the DeepL account dashboard for an active subscription and quota.
  4. 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

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

Related errors


AI-assisted analysis of SubtitleEdit/subtitleedit@17a9f07487 (2026-08-13). Data as JSON: /api/errors/f1c43e265fb406df. Report an issue: GitHub.