API reference

Billing

Mintoken paid plans are billed via Razorpay (INR primary). This page documents the client-side flow for integrating the Razorpay Checkout widget into your own dashboard. Backend endpoints + plan limits live on the Plans page.

The hosted dashboard already does this for you
The mintoken-hosted dashboard at /dashboard/billing handles this flow end-to-end. This page is for teams white-labeling mintoken into their own product, or for anyone implementing a custom upgrade UX (e.g. an in-product paywall in your IDE plugin).

The 3-step flow

  • Create an order (server-side): POST /v1/billing/razorpay/order with the plan slug. Returns order_id, amount in paise, and the public key_id safe to send to the browser.
  • Open Razorpay Checkout (client-side): pass the order_id and key_id to the Razorpay SDK, which renders the payment modal. User pays with UPI / card / netbanking / wallet.
  • Verify the signature (server-side): POST /v1/billing/razorpay/verify with the payment_id + signature returned by Checkout. Backend recomputes the HMAC and updates profiles.plan in Supabase. New quota live on the very next proxy request.
Never trust the client-side success callback alone
Razorpay Checkout calls your handler when the payment succeeds, but a malicious user can call that callback by hand without paying. The razorpay_signature is the proof; verify it on your server before flipping any feature flag.

Step 1: create the order

POST /v1/billing/razorpay/order

Authentication: Supabase JWT (the access_token from POST /v1/auth/login), not a mintoken key.

Body fields:

FieldTypeNotes
planstring"pro" or "team". Free / enterprise are not orderable through this endpoint.
curl -X POST https://api.mintoken.in/v1/billing/razorpay/order \
  -H "Authorization: Bearer <supabase-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"plan": "pro"}'

# Response:
# {
#   "order_id": "order_NXXX...",
#   "amount": 159900,
#   "currency": "INR",
#   "key_id": "rzp_live_XXX",
#   "plan": "pro"
# }

Errors

StatusMeaning
400Unsupported plan slug.
503Razorpay credentials missing on the server. This is a config issue, not a payment issue.
502Razorpay upstream rejected the order creation.

Step 2: open Razorpay Checkout (client-side)

Razorpay Checkout is a widget loaded from checkout.razorpay.com/v1/checkout.js. Load it once on the page; instantiate per-upgrade.

// 1. Load the Razorpay Checkout script (once, at the top of your page)
//    <script src="https://checkout.razorpay.com/v1/checkout.js"></script>

// 2. Get a fresh order from your backend (which calls /v1/billing/razorpay/order)
const order = await createOrder("pro");

// 3. Open Razorpay Checkout
const rzp = new window.Razorpay({
  key: order.key_id,
  amount: order.amount,
  currency: order.currency,
  name: "Mintoken",
  description: "Pro plan upgrade",
  order_id: order.order_id,
  prefill: {
    email: currentUser.email,
  },
  theme: { color: "#FF6B1A" },
  handler: async (response) => {
    // 4. Verify the signature server-side
    const verify = await fetch("/api/verify-upgrade", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        plan: "pro",
        razorpay_order_id: response.razorpay_order_id,
        razorpay_payment_id: response.razorpay_payment_id,
        razorpay_signature: response.razorpay_signature,
      }),
    });

    if (verify.ok) {
      // Plan is now upgraded. Refresh the user object.
      window.location.reload();
    }
  },
  modal: {
    ondismiss: () => {
      // User closed checkout without paying. No upgrade.
    },
  },
});

rzp.open();

Theme color #FF6B1A is the mintoken orange and is what the hosted dashboard uses. Replace with your brand color if you white-label.

Step 3: verify the signature (server-side)

POST /v1/billing/razorpay/verify

Body fields (all required):

FieldSource
planThe plan you originally created the order for. Must match.
razorpay_order_idFrom the Step 1 response.
razorpay_payment_idFrom the Razorpay Checkout success callback.
razorpay_signatureHMAC-SHA256 of order_id|payment_id signed with your Razorpay secret. Razorpay Checkout returns this; mintoken backend re-computes and compares.
curl -X POST https://api.mintoken.in/v1/billing/razorpay/verify \
  -H "Authorization: Bearer <supabase-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "plan": "pro",
    "razorpay_order_id": "order_NXXX...",
    "razorpay_payment_id": "pay_NYYY...",
    "razorpay_signature": "<hex-signature>"
  }'

# Response on success:
# {
#   "message": "Plan upgraded",
#   "old_plan": "free",
#   "new_plan": "pro",
#   "payment_id": "pay_NYYY..."
# }

# Response on bad signature:
# 400 { "detail": "Invalid payment signature" }

What happens on success

  • Signature is recomputed and matched.
  • profiles.plan is updated to the new plan in Supabase.
  • The new quota / RPM limits apply on the very next proxy request.
  • The upgrade is logged with old_plan -> new_plan and the payment id for the audit trail.

Server-side webhook (recommended)

The verify endpoint is sufficient for the happy path, but Razorpay also fires async webhooks at /v1/billing/razorpay/webhook for events like payment.captured, payment.failed, refund.created. The webhook is the source of truth for renewal billing once subscriptions ship; verify handles only the upgrade moment.

Today: paid plans are gated
Paid plans are temporarily off while we onboard early users on the free tier. The Razorpay endpoints described here exist and are smoke-tested, but the dashboard upgrade button is hidden. When paid plans return, this flow does not change; integrators built against this doc will keep working.