{"record":{"id":"c6b91601127c144b","repo":"TryGhost/Ghost","slug":"failed-to-update-email-preferences","errorCode":null,"errorMessage":"Failed to update email preferences","messagePattern":"Failed to update email preferences","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":467,"sourceCode":"                body.enable_comment_notifications = enableCommentNotifications;\n            }\n\n            if (enableUpdatesAndAnnouncements !== undefined) {\n                body.enable_updates_and_announcements = enableUpdatesAndAnnouncements;\n            }\n\n            return makeRequest({\n                url,\n                method: 'PUT',\n                headers: {\n                    'Content-Type': 'application/json'\n                },\n                body: JSON.stringify(body)\n            }).then(function (res) {\n                if (res.ok) {\n                    return res.json();\n                } else {\n                    throw new Error('Failed to update email preferences');\n                }\n            });\n        },\n\n        async updateEmailAddress({email}) {\n            const identity = await api.member.identity();\n            const url = endpointFor({type: 'members', resource: 'member/email'});\n            const body = {\n                email,\n                identity\n            };\n\n            return makeRequest({\n                url,\n                method: 'POST',\n                headers: {\n                    'Content-Type': 'application/json'\n                },","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L449-L485","documentation":"Thrown in api.member.updateEmailPreferences (apps/portal/src/utils/api.js:467) when PUT to /members/api/member/newsletters/ (uuid/key auth) returns non-ok. Message 'Failed to update email preferences'. Used by the unsubscribe/manage-preferences flow when the member is not signed in but has a uuid+key token.","triggerScenarios":"Visitor opens a manage-preferences link with uuid+key; Portal PUTs the updated newsletters list; the response is 4xx/5xx — token expired/used, member not found, no newsletters configured, or the body shape is invalid.","commonSituations":"The uuid/key token was already rotated or expired; member was deleted; the link was opened after the token TTL elapsed; CSRF/identity check failed; Portal bundle / server version mismatch on the newsletters endpoint contract.","solutions":["Verify the uuid and key in the URL are intact and unused (request a fresh magic link if expired).","Inspect the PUT response status/body — 404 = member gone, 401 = bad token, 422 = body invalid.","Confirm at least one enabled newsletter exists (Settings > Newsletter).","Re-prompt the user to sign in to manage preferences instead of relying on the token."],"exampleFix":"// before\nif (res.ok) { return res.json(); } else { throw new Error('Failed to update email preferences'); }\n\n// after\nif (res.ok) { return res.json(); }\nif (res.status === 401 || res.status === 404) { throw new Error('Your preferences link has expired — request a new one'); }\nthrow new Error(`Failed to update email preferences (${res.status})`);","handlingStrategy":"validation","validationCode":"// validate the token pair before calling\nfunction isValidManagePrefsToken({uuid, key}) {\n    return typeof uuid === 'string' && typeof key === 'string' && uuid.length > 0 && key.length > 0;\n}","typeGuard":null,"tryCatchPattern":"try {\n    const updated = await api.member.updateEmailPreferences({uuid, key, newsletters});\n    return updated;\n} catch (err) {\n    if (/expired/.test(err.message)) { promptSignIn(); }\n    else { notifyError('Could not update preferences — please try again'); }\n}","preventionTips":["Validate the uuid/key token pair before issuing the PUT.","Prompt the user to sign in when the token is expired.","Confirm at least one enabled newsletter exists.","Match Portal bundle and Ghost server versions on the preferences contract."],"tags":["portal","email-preferences","newsletters","members-api","token"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}