{"record":{"id":"c70903a12d3f3180","repo":"TryGhost/Ghost","slug":"failed-to-verify-code","errorCode":null,"errorMessage":"Failed to verify code","messagePattern":"Failed to verify code","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":402,"sourceCode":"            };\n\n            const res = await makeRequest({\n                url,\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify(body)\n            });\n\n            if (res.ok) {\n                return await res.json();\n            } else {\n                const humanError = await HumanReadableError.fromApiResponse(res);\n                if (humanError) {\n                    throw humanError;\n                }\n                throw new Error('Failed to verify code');\n            }\n        },\n\n        signout(all = false) {\n            const url = endpointFor({type: 'members', resource: 'session'});\n            return makeRequest({\n                url,\n                method: 'DELETE',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify({\n                    all\n                })\n            }).then(function (res) {\n                if (res.ok) {\n                    window.location.replace(siteUrl);\n                    return 'Success';","sourceCodeStart":384,"sourceCodeEnd":420,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L384-L420","documentation":"Thrown in api.member.verifyOTC (apps/portal/src/utils/api.js:402) when POST /members/api/verify-otc/ returns non-ok and HumanReadableError cannot be parsed. Message 'Failed to verify code'. Covers the one-time-code verification step of the new sign-in flow.","triggerScenarios":"Member enters the one-time code from their email; verify-otc responds 4xx/5xx without a JSON error body — typically a wrong/expired code (400), rate-limited guesses (429), or the otc_ref no longer matches a pending session.","commonSituations":"User typed the code wrong too many times; the code expired (usually >10 min); the otc_ref session was cleared; replay attempt of an already-used code; Portal bundle / server version mismatch in the OTC flow.","solutions":["Have the user request a fresh magic link and use the new code.","Inspect the response status — 400/422 = wrong code, 429 = slow down, 404 = otc_ref invalid.","Confirm Portal and Ghost server versions both support the OTC verify contract.","Ensure the integrity token is still valid when verify-otc is called."],"exampleFix":"// before\nthrow new Error('Failed to verify code');\n\n// after\nconst e = new Error(`Failed to verify code (${res.status})`);\ne.status = res.status;\nthrow e;","handlingStrategy":"validation","validationCode":"// 6-digit code shape check\nfunction isValidOtc(code) {\n    return typeof code === 'string' && /^\\d{6}$/.test(code.trim());\n}","typeGuard":null,"tryCatchPattern":"try {\n    const result = await api.member.verifyOTC({otc, otcRef, integrityToken});\n    return result;\n} catch (err) {\n    if (err.code === 'rate_limited') { notifyWait(); }\n    else { notifyError('Wrong or expired code — request a new link'); }\n}","preventionTips":["Validate the OTC shape (6 digits) before submitting.","Rate-limit verification attempts client-side to avoid 429s.","Re-mint the magic link when the code expires instead of guessing.","Confirm Portal and Ghost server both support the OTC verify contract."],"tags":["portal","otc","signin","members-api","verification"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}