TryGhost/Ghost · error · Error

redirectResult.error.message

Error message

redirectResult.error.message

What it means

Thrown inside checkoutPlan() after the Ghost backend successfully created a Stripe Checkout Session but Stripe.js's redirectToCheckout() rejected it. This is a Stripe.js client-side failure, not a Ghost backend failure — the sessionId/publicKey came back fine, but the browser could not hand off to Stripe's hosted checkout page.

Source

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

                if (!res.ok) {
                    const errData = await res.json();
                    const err = errData?.errors?.[0] || {};
                    const errMssg = err.message || 'Failed to signup, please try again.';
                    const error = new Error(errMssg);
                    error.code = err.code;
                    throw error;
                }
                return res.json();
            }).then(function (responseBody) {
                if (responseBody.url) {
                    return window.location.assign(responseBody.url);
                }
                const stripe = window.Stripe(responseBody.publicKey);
                return stripe.redirectToCheckout({
                    sessionId: responseBody.sessionId
                }).then(function (redirectResult) {
                    if (redirectResult.error) {
                        throw new Error(redirectResult.error.message);
                    }
                });
            });
        },

        async continueGiftCheckout() {
            const siteUrlObj = new URL(siteUrl);
            const identity = await api.member.identity();
            const url = endpointFor({type: 'members', resource: 'create-stripe-checkout-session'});

            const checkoutSuccessUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
            checkoutSuccessUrl.searchParams.set('stripe', 'success');

            const checkoutCancelUrl = window.location.href.startsWith(siteUrlObj.href) ? new URL(window.location.href) : new URL(siteUrl);
            checkoutCancelUrl.searchParams.set('stripe', 'cancel');

            const body = {
                type: 'subscription',

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Refresh the page and restart checkout — a fresh session is created each time and bypasses a stale/expired sessionId.
  2. Verify Stripe publishable/secret key parity in Ghost Admin settings (Stripe Connect integration) — a test key paired with live mode is the classic cause.
  3. Check browser console for Stripe.js load failures (network/CSP) and ensure the Stripe.js script tag is present and unblocked.
  4. Check status.stripe.com for a Checkout outage.
  5. If reproducible, capture redirectResult.error.code/type from Stripe (e.g. 'invalid_session_id') for a precise diagnosis.

Example fix

// before: throws raw Stripe error.message with no context
stripe.redirectToCheckout({sessionId}).then(r => { if (r.error) throw new Error(r.error.message); });

// after: classify the Stripe error so the UI can offer a retry
stripe.redirectToCheckout({sessionId}).then(r => {
    if (r.error) {
        const e = new Error(r.error.message);
        e.code = r.error.code;
        e.retryable = ['expired', 'invalid_session_id'].includes(r.error.code);
        throw e;
    }
});
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify Stripe.js loaded and the key matches before redirecting
function stripeReady() {
    return typeof window.Stripe === 'function' && !!window.Stripe._instance;
}
// Cannot validate sessionId expiry client-side; restart the flow to get a fresh one.

Type guard

function isStripeRedirectError(result) {
    return typeof result === 'object' && result !== null && typeof result.error === 'object' && typeof result.error.message === 'string';
}

Try / catch

try {
    await api.member.checkoutPlan({...});
} catch (err) {
    // Stripe redirect failed — offer a retry that mints a new session
    if (/session|stripe|checkout/i.test(err.message)) {
        offerRetry();
    } else {
        showToast(err.message);
    }
}

Prevention

When it happens

Trigger: stripe.redirectToCheckout({sessionId}) returns {error} — typically because the sessionId is malformed/expired, the publishable key (publicKey) doesn't match the account that created the session, Stripe.js failed to load, or the session was already consumed/expired (sessions expire 24h after creation).

Common situations: Stripe keys mismatched between Ghost Admin settings and the live site (test vs live key); clock drift causing the session to appear expired; Stripe.js script blocked by an ad-blocker or CSP; user opened checkout, left it >24h, then clicked again reusing a cached sessionId; Stripe partial outage.

Related errors


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