{"record":{"id":"50b94a5e88b6d2fd","repo":"TryGhost/Ghost","slug":"failed-to-send-magic-link-email","errorCode":null,"errorMessage":"Failed to send magic link email","messagePattern":"Failed to send magic link email","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":373,"sourceCode":"                body: JSON.stringify(body)\n            });\n\n            if (res.ok) {\n                const contentType = (res.headers.get('content-type') || '').toLowerCase();\n                if (contentType.includes('application/json')) {\n                    try {\n                        return await res.json();\n                    } catch (e) {\n                        // fall through to response used pre-OTC\n                    }\n                }\n                return {};\n            } else {\n                const humanError = await HumanReadableError.fromApiResponse(res);\n                if (humanError) {\n                    throw humanError;\n                }\n                throw new Error('Failed to send magic link email');\n            }\n        },\n\n        async verifyOTC({otc, otcRef, redirect, integrityToken}) {\n            const url = endpointFor({type: 'members', resource: 'verify-otc'});\n            const body = {\n                otc,\n                otcRef,\n                redirect,\n                integrityToken\n            };\n\n            const res = await makeRequest({\n                url,\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L355-L391","documentation":"Thrown in api.member.sendMagicLink (apps/portal/src/utils/api.js:373) when the magic-link request returns non-ok and HumanReadableError cannot be built from the response. Message 'Failed to send magic link email'. Covers the sign-in/up email-send path.","triggerScenarios":"Visitor submits an email for sign-in or signup; the POST to the magic-link endpoint responds 4xx/5xx without a JSON error envelope — e.g. invalid email format (client should pre-validate), rate limit, Email service down, member signup disabled.","commonSituations":"Free member signup disabled in settings; rate limit hit from repeated magic-link requests; Email provider misconfigured; reverse proxy returning an HTML error; Portal bundle out of sync with the backend magic-link contract.","solutions":["Validate the email client-side before submitting.","If rate-limited, wait before retrying; check the response status (typically 429).","Confirm 'Allow free member signup' is enabled in Settings > Membership when applicable.","Verify the Email service is configured and healthy in Settings > Email."],"exampleFix":"// before\nthrow new Error('Failed to send magic link email');\n\n// after\nconst e = new Error(`Failed to send magic link email (${res.status})`);\ne.status = res.status;\nthrow e;","handlingStrategy":"validation","validationCode":"// basic email format check before sending magic link\nfunction isValidEmail(email) {\n    return typeof email === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n}","typeGuard":null,"tryCatchPattern":"try {\n    await api.member.sendMagicLink({email});\n    notify('Check your email for a sign-in link');\n} catch (err) {\n    // err.message === 'Failed to send magic link email'\n    notifyError(err.message);\n}","preventionTips":["Validate email format client-side before submitting.","Throttle magic-link requests client-side to avoid server 429s.","Enable 'Allow free member signup' in Settings > Membership when applicable.","Verify Email service is configured and healthy."],"tags":["portal","magic-link","signin","members-api","email"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}