{"record":{"id":"a28fc307c50684c6","repo":"TryGhost/Ghost","slug":"failed-to-fetch-offer-data","errorCode":null,"errorMessage":"Failed to fetch offer data","messagePattern":"Failed to fetch offer data","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/portal/src/utils/api.js","lineNumber":116,"sourceCode":"                } else {\n                    throw new Error('Failed to fetch site data');\n                }\n            });\n        },\n\n        offer({offerId}) {\n            const url = contentEndpointFor({resource: `offers/${offerId}`});\n            return makeRequest({\n                url,\n                method: 'GET',\n                headers: {\n                    'Content-Type': 'application/json'\n                }\n            }).then(function (res) {\n                if (res.ok) {\n                    return res.json();\n                } else {\n                    throw new Error('Failed to fetch offer data');\n                }\n            });\n        },\n\n        recommendations({limit = 100} = {limit: 100}) {\n            const url = contentEndpointFor({resource: 'recommendations', params: {limit}});\n            return makeRequest({\n                url,\n                method: 'GET',\n                headers: {\n                    'Content-Type': 'application/json'\n                }\n            }).then(function (res) {\n                if (res.ok) {\n                    return res.json();\n                } else {\n                    throw new Error('Failed to fetch recommendations');\n                }","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/portal/src/utils/api.js#L98-L134","documentation":"Thrown in api.site.offer({offerId}) when GET {apiUrl}/offers/<id>/ returns non-ok. Message 'Failed to fetch offer data'. Used by Portal when a visitor opens an offer link and Portal needs to validate the offer.","triggerScenarios":"Visitor clicks an offer link; Portal calls the offer endpoint with an offerId; the response is 4xx/5xx — offer expired, archived, tier missing, or the offer was deleted.","commonSituations":"Offer already expired by the time the visitor opened the link; offer was deleted in admin; offer references a tier that no longer exists; invalid or expired Content API key; CORS / apiUrl misconfiguration.","solutions":["Confirm the offer still exists and is not expired/archived in Settings > Offers.","Validate the offerId in the URL is well-formed (UUID).","Check the Content API key and apiUrl configuration that Portal uses.","If the offer is legitimately gone, design the UI to fall back to the regular checkout page."],"exampleFix":"// before\nif (res.ok) { return res.json(); } else { throw new Error('Failed to fetch offer data'); }\n\n// after\nif (res.ok) { return res.json(); }\nif (res.status === 404) { throw new Error('Offer not found or expired'); }\nthrow new Error(`Failed to fetch offer data (${res.status})`);","handlingStrategy":"validation","validationCode":"// validate the offerId shape before calling\nfunction isValidOfferId(id) {\n    return typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);\n}","typeGuard":null,"tryCatchPattern":"try {\n    return await api.site.offer({offerId});\n} catch (err) {\n    if (/fetch offer/.test(err.message)) { redirectToRegularCheckout(); }\n    throw err;\n}","preventionTips":["Validate offerId shape before calling the endpoint.","Render a fallback checkout page when the offer is expired/archived (404).","Keep offer TTLs sensible in admin; archive instead of deleting.","Confirm the Content API key is current."],"tags":["portal","offers","content-api","validation"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}