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
- Verify the offer is backed by a synced Stripe tier/product (see the tiers sync flow) before redemption.
- Confirm `fakeCheckoutPage.waitUntilLoaded()` returned and `completeLatestSubscriptionCheckout` recorded a session.
- Inspect `stripe.getCheckoutSessions().length` to determine whether the issue is zero sessions or a missing field.
- 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
- Sync the offer's tier to Stripe before attempting redemption.
- Verify the fake checkout page fully loaded before calling `completeLatestSubscriptionCheckout`.
- Confirm the Stripe mock captures subscription-mode (not just payment-mode) checkouts.
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
- Latest Stripe checkout session does not include a success UR
- Latest Stripe checkout session does not include a success UR
- Invalid donation amount: ${value}
- Latest Stripe checkout session does not include a success UR
- Expected redeemed offer member ${emailAddress} to have a sub
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/e4d3060c5931906f.
Report an issue: GitHub.