TryGhost/Ghost · error · Error
Failed to fetch offer data
Error message
Failed to fetch offer data
What it means
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.
Source
Thrown at apps/portal/src/utils/api.js:116
} else {
throw new Error('Failed to fetch site data');
}
});
},
offer({offerId}) {
const url = contentEndpointFor({resource: `offers/${offerId}`});
return makeRequest({
url,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(function (res) {
if (res.ok) {
return res.json();
} else {
throw new Error('Failed to fetch offer data');
}
});
},
recommendations({limit = 100} = {limit: 100}) {
const url = contentEndpointFor({resource: 'recommendations', params: {limit}});
return makeRequest({
url,
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(function (res) {
if (res.ok) {
return res.json();
} else {
throw new Error('Failed to fetch recommendations');
}View on GitHub (pinned to 47d8b0e2ad)
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.
Example fix
// before
if (res.ok) { return res.json(); } else { throw new Error('Failed to fetch offer data'); }
// after
if (res.ok) { return res.json(); }
if (res.status === 404) { throw new Error('Offer not found or expired'); }
throw new Error(`Failed to fetch offer data (${res.status})`); Defensive patterns
Strategy: validation
Validate before calling
// validate the offerId shape before calling
function isValidOfferId(id) {
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);
} Try / catch
try {
return await api.site.offer({offerId});
} catch (err) {
if (/fetch offer/.test(err.message)) { redirectToRegularCheckout(); }
throw err;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Failed to update member
- Failed to fetch recommendations
- Failed to apply offer
- Email is not valid
- Theme is not compatible or contains errors.
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/a28fc307c50684c6.
Report an issue: GitHub.