koala73/worldmonitor · warning · ConvexError

PAYMENT_IN_PROGRESS

PAYMENT_IN_PROGRESS

Error message

A ${pending.displayName} payment is already in progress for this account. It may still be completing — finish it, or start a new checkout.

What it means

`createCheckout`'s second guard, `getCheckoutBlockingPendingPayment`, detects an in-flight payment for the same product and refuses with `buildPendingBlockedPayload(pending)` / `PAYMENT_IN_PROGRESS`. This prevents stacked charges from double-clicks or back-navigation. Unlike the subscription guard, it can be skipped by passing `bypassPendingGuard: true` after explicit user confirmation.

Source

Thrown at convex/payments/checkout.ts:371

      // 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.
    if (isCheckoutRateLimitedOutcome(result)) {
      throw new ConvexError({
        code: CHECKOUT_RATE_LIMITED,

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Wait for the in-progress payment to settle, then retry if needed
  2. If the user has explicitly confirmed 'start a new checkout anyway', pass `bypassPendingGuard: true` (the bypass is audit-logged server-side)
  3. Disable the purchase button after first click to prevent duplicate submission

Example fix

// before
createCheckout({ productId })
// after — user-confirmed override
createCheckout({ productId, bypassPendingGuard: true })
Defensive patterns

Strategy: validation

Validate before calling

// Disable the purchase button after first click to avoid a second in-flight payment.
let submitting = false;
async function onPurchase() {
  if (submitting) return;
  submitting = true;
  try { await convex.action(api.payments.checkout.createCheckout, args); }
  finally { submitting = false; }
}

Try / catch

try {
  await convex.action(api.payments.checkout.createCheckout, args);
} catch (err) {
  if (err.data?.code === 'PAYMENT_IN_PROGRESS') {
    // offer 'finish existing' or, with explicit user confirm, retry with bypassPendingGuard: true
  } else { throw err; }
}

Prevention

When it happens

Trigger: Starting a second checkout while a previous payment for the same product is still completing; double-click on the purchase button; navigating back and re-submitting.

Common situations: Accidental double-click; user re-checks out after a slow provider response; stale pending-payment row left by an abandoned session.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/536a0d1d077c5dc4. Report an issue: GitHub.