koala73/worldmonitor · warning · ConvexError
ACTIVE_SUBSCRIPTION_EXISTS
ACTIVE_SUBSCRIPTION_EXISTS
Error message
A ${subscription.displayName} subscription already exists for this account. Use Manage Billing to update it instead of purchasing again. What it means
`createCheckout` runs `getCheckoutBlockingSubscription` first; if the user already has an active subscription that blocks this product, checkout is refused and the error carries `buildBlockedCheckoutPayload(blocking)` with `ACTIVE_SUBSCRIPTION_EXISTS`. The message directs the user to Manage Billing rather than re-purchasing. This guard always runs, even with `bypassPendingGuard`.
Source
Thrown at convex/payments/checkout.ts:368
const identity = await resolveUserIdentity(ctx);
if (args.bypassPendingGuard) {
// Audit trail: the user confirmed "start a new checkout anyway" past a
// pending-payment block. Logged server-side so a future double-charge
// investigation has the bypass record (#4438 review — the original
// incident was undetected stacked payments).
console.info(`[checkout] pending-payment guard bypassed user=${userId} product=${args.productId}`);
}
// Run both guards concurrently — they share no data, so serial awaits only
// add a Convex round-trip to every checkout (#4438 review). Subscription
// block still WINS (evaluated first); bypass skips the pending query.
const [blocking, pending] = await Promise.all([
getCheckoutBlockingSubscription(ctx, userId, args.productId),
args.bypassPendingGuard
? Promise.resolve(null)
: getCheckoutBlockingPendingPayment(ctx, userId, args.productId),
]);
if (blocking) {
throw new ConvexError(buildBlockedCheckoutPayload(blocking));
}
if (pending) {
throw new ConvexError(buildPendingBlockedPayload(pending));
}
const customerName = identity
? [identity.givenName, identity.familyName].filter(Boolean).join(" ") ||
identity.name
: undefined;
const result = await _createCheckoutSession(args, {
userId,
email: identity?.email,
name: customerName,
});
// The public Convex action historically rejects provider failures. Keep
// that error-channel contract: only the trusted internal relay consumes
// the typed outcome and translates it into HTTP 429 + Retry-After.View on GitHub (pinned to ffec79ac33)
Solutions
- Use Manage Billing to update/cancel the existing subscription instead of purchasing again
- Cancel or let the existing subscription lapse before re-purchasing
- Reflect the existing subscription in the purchase UI to prevent the attempt
Defensive patterns
Strategy: validation
Validate before calling
// Reflect existing subscription state before offering purchase.
const sub = await convex.query(api.payments.subscriptions.activeForProduct, { productId: args.productId });
if (sub) { /* route to Manage Billing instead of createCheckout */ } Try / catch
try {
await convex.action(api.payments.checkout.createCheckout, args);
} catch (err) {
if (err.data?.code === 'ACTIVE_SUBSCRIPTION_EXISTS') {
// redirect to Manage Billing
} else { throw err; }
} Prevention
- Show 'Manage Billing' instead of 'Purchase' when an active subscription exists
- Refresh subscription state before presenting the purchase UI
When it happens
Trigger: Calling `createCheckout` for a product the user already actively subscribes to; attempting to stack an identical subscription.
Common situations: User forgot they are already subscribed; double-purchase attempt; UI failed to reflect existing subscription state.
Related errors
- PAYMENT_IN_PROGRESS
- Dodo checkout session ${session.session_id} has no checkout_
- BUSINESS_NOT_ACTIVE
- Invalid returnUrl: must be a valid absolute URL
- Invalid returnUrl: must use a trusted worldmonitor.app origi
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/4e6b2f62895e66c5.
Report an issue: GitHub.