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

  1. Confirm the offer still exists and is not expired/archived in Settings > Offers.
  2. Validate the offerId in the URL is well-formed (UUID).
  3. Check the Content API key and apiUrl configuration that Portal uses.
  4. 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

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


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/a28fc307c50684c6. Report an issue: GitHub.