koala73/worldmonitor · error · ConvexError

INVALID_CHECKOUT_PRODUCT

INVALID_CHECKOUT_PRODUCT

Error message

This product is not available for checkout.

What it means

requireCheckoutProduct validates that the requested Dodo product ID maps to a catalog entry that is currently offered (currentForCheckout) and purchasable self-serve (selfServe). Anything else — unknown product ID, retired plan, or internal/admin-only product — is rejected so checkout can never be created for an unintended product.

Solutions

  1. Fetch the current checkout-eligible product IDs from PRODUCT_CATALOG (or the pricing endpoint) and use one of those.
  2. If the plan was superseded, switch to the replacement product's dodoProductId.
  3. For non-self-serve products, use the internal/admin grant path instead of checkout.
  4. Clear stale client caches/pricing pages that reference retired product IDs.

Example fix

// before
createCheckout({ productId: "prod_old_pro_monthly" });  // retired
// after
const sku = await getCurrentSelfServeProductId("pro_monthly");
createCheckout({ productId: sku });
Defensive patterns

Strategy: validation

Validate before calling

import { PRODUCT_CATALOG } from "./catalog";
const ok = Object.values(PRODUCT_CATALOG).some(p => p.dodoProductId === productId && p.currentForCheckout && p.selfServe);
if (!ok) throw new Error(`product ${productId} is not checkout-eligible`);

Try / catch

try {
  await createCheckout({ productId });
} catch (e) {
  if (isConvexError(e) && e.data?.code === "INVALID_CHECKOUT_PRODUCT") {
    refreshPricingAndPromptUserToRechoosePlan();
  }
}

Prevention

When it happens

Trigger: Calling createCheckout or internalCreateCheckout with a productId that is not in PRODUCT_CATALOG, has dodoProductId mismatch, currentForCheckout=false (retired/superseded plan), or selfServe=false (comp/admin-only SKU).

Common situations: Client hardcodes an old dodoProductId after the plan was rotated; attempting to buy an enterprise/annual-only SKU through the self-serve checkout; typo'd product ID; stale cached pricing page pointing at a retired product.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-15). Data as JSON: /api/errors/2b23e9e08f03fd74. Report an issue: GitHub.

Appendix: source

Thrown at convex/payments/checkout.ts:88

          retryAfterSeconds: Math.max(1, Math.ceil((row.windowStart + CHECKOUT_ADMISSION_WINDOW_MS - now) / 1000)),
        };
      }
      await ctx.db.patch(row._id, { count: row.count + 1 });
    } else if (row) {
      await ctx.db.patch(row._id, { windowStart: now, count: 1 });
    } else {
      await ctx.db.insert("checkoutAdmissions", { userId, windowStart: now, count: 1 });
    }
    return null;
  },
});

function requireCheckoutProduct(productId: string): void {
  const allowed = Object.values(PRODUCT_CATALOG).some(
    (entry) => entry.dodoProductId === productId && entry.currentForCheckout && entry.selfServe,
  );
  if (!allowed) {
    throw new ConvexError({
      code: "INVALID_CHECKOUT_PRODUCT",
      message: "This product is not available for checkout.",
    });
  }
}

// RFC 5321 maximum forward-path length. A value beyond it is not an address we
// could deliver to anyway, and it keeps the stamped metadata value small.
const MAX_LOGIN_EMAIL_LENGTH = 254;

/**
 * Normalizes the authenticated login email for stamping into checkout metadata
 * (#6335).
 *
 * This is a shape guard, not a trust boundary — it keeps an unusable value out
 * of a field the webhook later hands to Resend as a recipient. What makes that
 * the right level: `createCheckout` reads the email from the Clerk JWT `email`
 * claim via `resolveUserIdentity`, and `internalCreateCheckout` receives it from

View on GitHub (pinned to e586b8b4b8)