TryGhost/Ghost · error · Error

Latest Stripe checkout session does not include a success UR

Error message

Latest Stripe checkout session does not include a success URL

What it means

Identical guard to the donations flow, thrown inside `redeemOfferViaStripeCheckout` after `stripe.completeLatestSubscriptionCheckout`. The offer-redeeming subscription checkout must yield a `success_url` so the browser can navigate to the confirmation. Failure means no session was captured or the captured session omits `success_url`.

Source

Thrown at e2e/helpers/playwright/flows/offers.ts:66

}

export async function completeOfferSignupViaPortal(page: Page, stripe: StripeTestService, opts?: {emailAddress?: string; name?: string}): Promise<{emailAddress: string; name: string}> {
    const offerPage = new PortalOfferPage(page);
    const emailAddress = opts?.emailAddress ?? `test${faker.string.uuid()}@ghost.org`;
    const name = opts?.name ?? faker.person.fullName();

    await offerPage.fillAndSubmit(emailAddress, name);
    await offerPage.continueIfVisible();

    const fakeCheckoutPage = new FakeStripeCheckoutPage(page);
    await fakeCheckoutPage.waitUntilLoaded();
    await stripe.completeLatestSubscriptionCheckout({name});

    const latestCheckoutSession = stripe.getCheckoutSessions().at(-1);
    const successUrl = latestCheckoutSession?.response.success_url;

    if (!successUrl) {
        throw new Error('Latest Stripe checkout session does not include a success URL');
    }

    await page.goto(successUrl);

    return {emailAddress, name};
}

export async function redeemOfferViaPortal(page: Page, stripe: StripeTestService, opts?: {
    emailAddress?: string;
    name?: string;
}): Promise<{
    accountPage: PortalAccountPage;
    emailAddress: string;
    name: string;
    subscription: MemberSubscription;
}> {
    const membersService = new MembersService(page.request);
    const {emailAddress, name} = await completeOfferSignupViaPortal(page, stripe, opts);

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Verify the offer is backed by a synced Stripe tier/product (see the tiers sync flow) before redemption.
  2. Confirm `fakeCheckoutPage.waitUntilLoaded()` returned and `completeLatestSubscriptionCheckout` recorded a session.
  3. Inspect `stripe.getCheckoutSessions().length` to determine whether the issue is zero sessions or a missing field.
  4. Check the Stripe test service is capturing subscription-mode checkouts, not just payment-mode.
Defensive patterns

Strategy: validation

Validate before calling

const sessions = stripe.getCheckoutSessions();
if (!sessions.length || !sessions.at(-1)?.response?.success_url) {
    throw new Error(`Offer checkout not recorded (${sessions.length} sessions)`);
}

Type guard

function isCheckoutSessionWithSuccessUrl(s: unknown): s is {response: {success_url: string}} {
    return !!s && typeof (s as any).response?.success_url === 'string';
}

Prevention

When it happens

Trigger: The offer redemption flow submits `FakeStripeCheckoutPage`, then `completeLatestSubscriptionCheckout` runs, but `getCheckoutSessions()` is empty or the last session's `response.success_url` is missing.

Common situations: Offer was not created with a valid Stripe price/product, so no real checkout session is generated; the fake Stripe server webhook for `checkout.session.completed` did not register the session; subscription checkout path differs from one-off payment path and the mock did not record it.

Related errors


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