TryGhost/Ghost · error · Error
Expected redeemed offer member ${emailAddress} to have a sub
Error message
Expected redeemed offer member ${emailAddress} to have a subscription What it means
Thrown in `redeemOfferViaPortal` after fetching the member via `membersService.getByEmailWithSubscriptions(emailAddress)`. It asserts `member.subscriptions[0]` exists — the offer redemption is expected to create an active subscription. A missing subscription means the checkout completed in the UI but Ghost's backend never recorded a subscription.
Source
Thrown at e2e/helpers/playwright/flows/offers.ts:98
name: string;
subscription: MemberSubscription;
}> {
const membersService = new MembersService(page.request);
const {emailAddress, name} = await completeOfferSignupViaPortal(page, stripe, opts);
const homePage = new HomePage(page);
await homePage.goto();
await homePage.openAccountPortal();
const accountPage = new PortalAccountPage(page);
await accountPage.waitForPortalToOpen();
await accountPage.emailText(emailAddress).waitFor({state: 'visible'});
const member = await membersService.getByEmailWithSubscriptions(emailAddress);
const subscription = member.subscriptions[0];
if (!subscription) {
throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
}
return {
accountPage,
emailAddress,
name,
subscription
};
}
View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Add a short retry/poll loop around `getByEmailWithSubscriptions` to wait for webhook processing to persist the subscription.
- Confirm the Stripe webhook client in the test environment is delivering `customer.subscription.created` events.
- Verify the success URL navigation actually triggered Ghost's subscription-creation endpoint.
- Check that the offer is configured to grant a paid tier subscription, not just a discount.
Example fix
// before
const member = await membersService.getByEmailWithSubscriptions(emailAddress);
const subscription = member.subscriptions[0];
if (!subscription) {
throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
}
// after — wait for webhook to land
let member = await membersService.getByEmailWithSubscriptions(emailAddress);
for (let i = 0; i < 20 && !member.subscriptions?.length; i++) {
await new Promise(r => setTimeout(r, 500));
member = await membersService.getByEmailWithSubscriptions(emailAddress);
}
const subscription = member.subscriptions?.[0];
if (!subscription) {
throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
} Defensive patterns
Strategy: retry
Type guard
function memberHasSubscription(m: unknown): m is {subscriptions: unknown[]} {
return !!m && Array.isArray((m as any).subscriptions) && (m as any).subscriptions.length > 0;
} Try / catch
let member = await membersService.getByEmailWithSubscriptions(emailAddress);
for (let i = 0; i < 20 && !memberHasSubscription(member); i++) {
await new Promise(r => setTimeout(r, 500));
member = await membersService.getByEmailWithSubscriptions(emailAddress);
}
if (!memberHasSubscription(member)) {
throw new Error(`Expected redeemed offer member ${emailAddress} to have a subscription`);
} Prevention
- Poll the members API for the subscription to absorb webhook processing latency.
- Confirm the Stripe webhook client delivers `customer.subscription.created` events.
- Verify the offer grants a paid-tier subscription before redeeming.
When it happens
Trigger: Offer redemption navigated to the success URL, but `member.subscriptions` is empty or undefined when re-fetched from the members API. Typically a Stripe webhook (`invoice.paid` / `customer.subscription.created`) did not arrive or was not processed before the lookup.
Common situations: Stripe webhook delivery is slow or dropped in the test environment; the member was created but the subscription is still pending in Stripe; race between `page.goto(successUrl)` completing and backend subscription persistence; webhook signing secret mismatch.
Related errors
- Timed out waiting for Stripe sync for tier ${tier.name}
- Latest Stripe checkout session does not include a success UR
- Latest Stripe checkout session does not include a success UR
- Latest Stripe checkout session does not include a success UR
- Invalid donation amount: ${value}
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/cf9ac06a8b2c1b32.
Report an issue: GitHub.