{"record":{"id":"f1c43e265fb406df","repo":"SubtitleEdit/subtitleedit","slug":"forbidden-resultcontent","errorCode":null,"errorMessage":"Forbidden! \n\n {resultContent}","messagePattern":"Forbidden! \n\n (.+?)","errorType":"exception","errorClass":"Exception","httpStatus":403,"severity":"error","filePath":"src/libuilogic/AutoTranslate/DeepLTranslate.cs","lineNumber":180,"sourceCode":"            var resultContent = string.Empty;\n            for (var attempt = 0; attempt <= retryDelays.Length; attempt++)\n            {\n                var postContent = MakeContent(text, sourceLanguageCode, targetLanguageCode);\n                result = await _httpClient.PostAsync(\"/v2/translate\", postContent, cancellationToken);\n                resultContent = await result.Content.ReadAsStringAsync(cancellationToken);\n\n                if (!ShouldRetry(result, resultContent) || attempt == retryDelays.Length)\n                {\n                    break;\n                }\n\n                await Task.Delay(retryDelays[attempt], cancellationToken);\n            }\n\n            if (result.StatusCode == HttpStatusCode.Forbidden)\n            {\n                Error = resultContent;\n                throw new Exception(\"Forbidden! \" + Environment.NewLine + Environment.NewLine + resultContent);\n            }\n\n            if (!result.IsSuccessStatusCode)\n            {\n                Error = resultContent;\n                SeLogger.Error(\"DeepLTranslate error: \" + resultContent);\n                throw new Exception($\"DeepL failed with status code {(int)result.StatusCode} ({result.StatusCode})\" + Environment.NewLine + Environment.NewLine + resultContent);\n            }\n\n            try\n            {\n                var resultList = new List<string>();\n                var parser = new JsonParser();\n                var x = (Dictionary<string, object>)parser.Parse(resultContent);\n                foreach (var k in x.Keys)\n                {\n                    if (x[k] is List<object> mainList)\n                    {","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/SubtitleEdit/subtitleedit/blob/17a9f0748781032255db3526b7215d2fb891e3af/src/libuilogic/AutoTranslate/DeepLTranslate.cs#L162-L198","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (result.StatusCode == HttpStatusCode.Forbidden)\n{\n    Error = resultContent;\n    throw new Exception(\"Forbidden! \" + Environment.NewLine + Environment.NewLine + resultContent);\n}\n\n// after - distinguish auth-plan mismatch from revoked key using DeepL error JSON\nif (result.StatusCode == HttpStatusCode.Forbidden)\n{\n    Error = resultContent;\n    var hint = resultContent.Contains(\"\\\"message\\\":\\\"Quota exceeded\", StringComparison.Ordinal)\n        ? \"DeepL quota exhausted - check your plan usage.\"\n        : \"DeepL rejected the API key - verify it matches this endpoint (free vs pro).\";\n    throw new Exception($\"DeepL Forbidden: {hint}\" + Environment.NewLine + Environment.NewLine + resultContent);\n}","handlingStrategy":"validation","validationCode":"// Before calling Translate, validate the DeepL key/endpoint pairing\nvar key = Configuration.Settings.Tools.DeepLApiKey;\nvar endpoint = Configuration.Settings.Tools.DeepLUrl ?? string.Empty;\nif (string.IsNullOrWhiteSpace(key))\n    throw new InvalidOperationException(\"DeepL API key is not set.\");\nvar isFreeKey = key.EndsWith(\":fx\", StringComparison.Ordinal);\nvar isFreeEndpoint = endpoint.Contains(\"api-free.deepl.com\", StringComparison.OrdinalIgnoreCase);\nif (isFreeKey != isFreeEndpoint)\n    throw new InvalidOperationException($\"DeepL key/endpoint mismatch: free keys must use api-free.deepl.com, pro keys must use api.deepl.com. Endpoint: {endpoint}\");","typeGuard":"public static bool IsDeepLConfigured(string key, string endpoint)\n{\n    if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(endpoint)) return false;\n    var isFreeKey = key.EndsWith(\":fx\");\n    var isFreeEndpoint = endpoint.Contains(\"api-free.deepl.com\");\n    return isFreeKey == isFreeEndpoint;\n}","tryCatchPattern":"try\n{\n    return await deeLTranslate.Translate(text, src, tgt, token);\n}\ncatch (Exception ex) when (ex.Message.StartsWith(\"Forbidden!\", StringComparison.Ordinal))\n{\n    // 403 from DeepL: prompt the user to re-check key + endpoint pairing\n    throw new InvalidOperationException(\"DeepL returned 403 - verify the API key matches the endpoint (free vs pro).\", ex);\n}","preventionTips":["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."],"tags":["network","api","auth","translation","deepl"],"backgroundTag":null,"analyzedSha":"17a9f0748781032255db3526b7215d2fb891e3af","analyzedAt":"2026-08-13T18:11:43.374Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}