TryGhost/Ghost · error · Error

Failed to apply offer

Error message

Failed to apply offer

What it means

Thrown by applyOffer() when the POST to apply an offer (members 'offers' resource) returns non-2xx. Unlike the other handlers, this reads the body as raw text (res.text()) and uses it as the error message, only falling back to 'Failed to apply offer' when the body is empty. Offers are discounted pricing plans redeemable by eligible members.

Source

Thrown at apps/portal/src/utils/api.js:873

        async applyOffer({offerId, subscriptionId}) {
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: `subscriptions/${subscriptionId}/apply-offer`});

            const res = await makeRequest({
                url,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    identity,
                    offer_id: offerId
                })
            });

            if (!res.ok) {
                const errorText = await res.text();
                throw new Error(errorText || 'Failed to apply offer');
            }

            return true;
        }
    };

    api.init = async () => {
        let [member] = await Promise.all([
            api.member.sessionData()
        ]);
        let site = {};
        let newsletters = [];
        let tiers = [];
        let settings = {};
        let offers = [];

        try {
            [{settings}, {tiers}, {newsletters}] = await Promise.all([

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Read the raw text thrown — applyOffer surfaces the backend's plain-text reason, which is usually specific (e.g. 'Offer no longer available').
  2. Confirm the offer is still active and not archived in Ghost Admin → Offers.
  3. Verify the member is eligible (offers often target members who are not already paid).
  4. Re-authenticate the member and retry the offer link.
  5. Check that the offer_id passed matches a real, active offer.

Example fix

// before: raw backend text (may be HTML or unhelpful) used directly as the message
const errorText = await res.text();
throw new Error(errorText || 'Failed to apply offer');

// after: try structured JSON first, fall back to text
let detail = '';
try {
    const errData = await res.json();
    detail = errData?.errors?.[0]?.message || '';
} catch { detail = await res.text().catch(() => ''); }
throw new Error(detail || 'Failed to apply offer');
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the offer id and member eligibility client-side before applying
function validOfferId(id) {
    return typeof id === 'string' && id.length > 0;
}
async function memberIsEligible(api) {
    try {
        const member = await api.member.sessionData();
        // Offers typically target free/non-paid members
        return !member?.subscriptions?.some(s => s.status === 'active');
    } catch {
        return false;
    }
}

Try / catch

try {
    await api.member.applyOffer(offerId);
} catch (err) {
    // err.message is the backend's raw text — usually specific
    showToast(err.message);
}

Prevention

When it happens

Trigger: Member clicks an offer redemption link; the backend rejects — the offer is expired/archived, the member is ineligible (already paid, or the offer targets a different segment), the offer_id is invalid, or the member's identity doesn't match the offer's constraints.

Common situations: Offer was archived in Ghost Admin after the link was generated; member already on a paid subscription (offer only targets free/eligible members); offer redemption limit reached; identity token expired; the offer_id in the URL is malformed. Per Portal's CONTEXT.md, ineligible visitors have the link silently ignored — this error only fires for visitors who reached the apply step.

Related errors


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