application/jsoncoupon_code is validated and applied; if omitted, a prior sticky coupon on the same plan is auto-reapplied (locked-in renewal pricing). requires_payment=false means the purchase was free and is already active - no Razorpay step needed.pending purchase (within PENDING_PURCHASE_TTL_MINUTES, default 60), it's reconciled against Razorpay live before deciding what to do - never blindly reopened for a fresh payment attempt. Three outcomes: (1) Razorpay confirms it was already paid -> activated on the spot, response has requires_payment: false, status: "active" (skip straight to step 2, no purchase needed); (2) Razorpay shows a live but not-yet-captured attempt (authorized moments ago, webhook hasn't landed) -> requires_payment: false, status: "processing", reason: "<user-facing message>" - show that message and do NOT reopen the payment widget, the user already has money on hold; (3) genuinely abandoned (dismissed the widget, never attempted payment) -> the SAME order/subscription is returned for a fresh attempt, exactly as before. This means a double-tapped 'Buy' never creates duplicate Razorpay orders AND never shows 'purchase again' to someone who already paid. Once the TTL window passes, the abandoned row is swept to failed by a daily cron and the next call mints a fresh one.requires_payment: false, status: "active" with that SAME purchase_key instead of opening Razorpay again. In other words: it is always safe for the frontend to call this endpoint again on any retry flow, including after a report-generation failure - you never need to special-case "did they already pay for this" client-side, the backend always knows. A credit only stops being reusable once its report actually reaches done.checkout_type changes which fields you use: read checkout_type ("order" vs "subscription") off the response and branch - the two flows do NOT share field names, mixing them up breaks the widget/payment callback (seen live 2026-08-20 on Guru yearly: widget opened, payment went through, then a client-side "payment id missing" error because the success handler assumed an order-shaped response).checkout_type: "order" (topup, one-time report): pass { key: razorpay_key_id, order_id: razorpay_order_id, amount, currency } into Checkout.js. On success the handler receives { razorpay_payment_id, razorpay_order_id, razorpay_signature } - send all three, as razorpay_order_id, to /payments/verify.checkout_type: "subscription" (Sage/Guru recurring): pass { key: razorpay_key_id, subscription_id: razorpay_subscription_id } into Checkout.js - do NOT pass order_id/amount/currency, there is no razorpay_order_id in this response at all. On success the handler receives { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } - note there is no order_id here either - send those three, as razorpay_subscription_id, to /payments/verify.requires_payment=false) or after /payments/verify confirms payment (requires_payment=true), the frontend must separately call that report's own generate endpoint (e.g. POST /palm/user, POST /marriage/report) as step 2 - THAT call is what finds the now-active credit and actually starts generation. This is true even for a 100%-off coupon: it skips the Razorpay widget in the middle, but step 2 still has to be called explicitly - nothing auto-generates on checkout alone.features.total_message/features.messages is > 0 (topups, and one-time reports like tarot/palm/marriage/child) seeds a messages_remaining balance on this purchase at checkout time - paid or free, doesn't matter. Once the user's daily plan quota (messages_per_day) runs out on any day, Nandini chat draws 1 from the OLDEST purchase with a remaining balance (FIFO across topups and report purchases alike) before blocking - this balance never resets and carries forward indefinitely until fully drawn down.subscription_slug, coupon_code (optional)200 - checkout details for the Razorpay Checkout widget, or an already-active purchase. valid: false with a reason ("Invalid subscription plan" for a bad subscription_slug, or "This plan is not currently available" if it's been deactivated) means nothing was created - requires_payment and purchase_key are then meaningless/absent. This mirrors /coupons/validate: a bad subscription_slug is a client input mistake, never a 404.404 - authenticated user record not found (should not normally happen) | 422 - coupon invalid, or plan not yet synced with Razorpay