docs.checkout.payments

Payment modes

QuantaRoute supports two payment paths. Mode A and Mode B are available now.

Two payment modes

Choose the mode that matches your existing payment stack. QuantaRoute never holds merchant funds long-term in any mode — customer payment settlement stays with your PG or Razorpay account.

Mode A — External PGMode B — BYO Razorpay
ProfilePayU, Cashfree, Paytm, ShopifyOwn Razorpay account
Widget paymentNo `enablePayment``enablePayment` + own keys
Portal setupBYO PG guidePaste Razorpay keys
SettlementYour PG → your bankYour Razorpay → your bank
`session.completed` webhookNoYes
Ship statusAvailable nowAvailable now
Need help choosing? Use Mode A if you already have PayU, Cashfree, Paytm, or Shopify checkout. Use Mode B if you have your own Razorpay account and want payment inside the widget.

Mode A — External payment gateway

QuantaRoute handles address capture, buyer identity, and DigiPin verification. Your existing PG handles payment and settlement.

Omit enablePayment (default is false). No QuantaRoute payment keys or Razorpay configuration required. Funds flow entirely through your existing PG to your bank account.

Widget embed

tsx
import { CheckoutWidget } from '@quantaroute/checkout';
import type { CompleteAddress } from '@quantaroute/checkout';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';

function buildMerchantCheckoutUrl(params: {
  sessionId: string;
  digipin: string;
  formattedAddress: string;
  pincode: string;
}) {
  const url = new URL('https://yourstore.com/checkout/pay');
  url.searchParams.set('session', params.sessionId);
  url.searchParams.set('digipin', params.digipin);
  url.searchParams.set('address', params.formattedAddress);
  url.searchParams.set('pincode', params.pincode);
  return url.toString();
}

export default function AddressStep() {
  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      merchantId={process.env.NEXT_PUBLIC_MERCHANT_ID!}
      supabaseFunctionBaseUrl={process.env.NEXT_PUBLIC_SUPABASE_FUNCTION_URL!}
      enableEmailAuth
      // enablePayment omitted — merchant handles PG externally
      onComplete={(address: CompleteAddress) => {
        window.location.href = buildMerchantCheckoutUrl({
          sessionId: address.sessionId ?? '',
          digipin: address.digipin,
          formattedAddress: address.formattedAddress,
          pincode: address.pincode,
        });
      }}
    />
  );
}

onComplete handoff

When the buyer submits a verified delivery address, onComplete fires with a CompleteAddress object. Use sessionId as the correlation key between QuantaRoute events and your draft order.

text
https://merchant.com/checkout/pay?session=<sessionId>&digipin=<digipin>&address=<formattedAddress>&pincode=<pincode>

Lazy DigiPin verify

If a buyer selects a saved address imported without a DigiPin, the widget shows a map pin step before onComplete fires. Do not create a shippable order until handoff completes with a verified DigiPin.

Address stateBuyer experience
Has DigiPinOTP → pick address → onComplete immediately
Missing DigiPin (imported)OTP → pick address → map pin + confirm form → onComplete
Same address on next orderSkips verification — DigiPin already stored
session.completed is not fired in Mode A. Mark orders paid after your PG confirms payment. See Webhooks & events for Mode A webhook events.

Mode B — BYO Razorpay

Buyer pays inside the QuantaRoute widget using your Razorpay account. Settlement goes directly to your bank.

Set enablePayment={true} and configure Razorpay keys server-side via the checkout-admin API. The widget calls checkout-payment to create orders and verify payments — keys never ship to the browser bundle.

Configure Razorpay keys

bash
curl -X PATCH \
  "https://<project>.supabase.co/functions/v1/checkout-admin?action=set_payment_config" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_MERCHANT_ADMIN_KEY" \
  -d '{
    "razorpay_key_id": "rzp_test_...",
    "razorpay_secret_key": "YOUR_TEST_SECRET"
  }'

Verify with GET checkout-admin?action=get_payment_config — returns razorpay_key_id_set: true. The secret is never returned.

Widget embed with payment

tsx
import { CheckoutWidget } from '@quantaroute/checkout';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@quantaroute/checkout/style.css';

export default function CheckoutPage() {
  const cartTotalPaise = 499_00; // ₹499.00

  return (
    <CheckoutWidget
      apiKey={process.env.NEXT_PUBLIC_QUANTAROUTE_KEY!}
      merchantId={process.env.NEXT_PUBLIC_MERCHANT_ID!}
      supabaseFunctionBaseUrl={process.env.NEXT_PUBLIC_SUPABASE_FUNCTION_URL!}
      enableEmailAuth
      enablePayment
      paymentAmountPaise={cartTotalPaise}
      orderConfirmationUrl="https://yourstore.com/order/confirm?sid={{sessionId}}"
      onComplete={(address) => {
        console.log(address.digipin, address.formattedAddress);
      }}
      onPaymentComplete={(result) => {
        console.log(result.paymentMethod);
        console.log(result.razorpayOrderId, result.razorpayPaymentId);
      }}
    />
  );
}
After successful prepaid or COD payment, QuantaRoute fires a session.completed webhook to your backend. See Webhooks & events.

Payment props reference

Payment-related widget props. Core address and auth props are documented on the Widget page.

PropTypeDefaultDescription
enablePaymentbooleanfalseShow Razorpay payment step after address confirmation (Mode B). Omit for Mode A external PG handoff.
paymentAmountPaisereqnumberCart total in paise (integer). Required when enablePayment is true.
enableCodbooleanfalseShow Cash on Delivery option when merchant COD config is enabled.
codCartTotalPaisenumberCart total used for COD eligibility checks (cod_max_cart_value_paise, blocked pincodes).
onPaymentComplete(result: PaymentResult) => voidCalled after prepaid Razorpay success or COD selection. Receives paymentMethod, razorpayOrderId, razorpayPaymentId.
orderConfirmationUrlstring (URL)Browser redirect after successful payment. {{sessionId}} is replaced with the checkout session ID.

Cash on Delivery (COD)

Enable COD on the merchant record, then pass enableCod on the widget. COD eligibility respects cod_blocked_pincodes and cod_max_cart_value_paise configured via checkout-admin.

bash
curl -X PATCH \
  "https://<project>.supabase.co/functions/v1/checkout-admin?action=cod_config" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_MERCHANT_ADMIN_KEY" \
  -d '{ "enable_cod": true }'
tsx
<CheckoutWidget
  /* ...base props... */
  enableCod
  codCartTotalPaise={cartTotalPaise}
  enablePayment
  paymentAmountPaise={cartTotalPaise}
/>

Selecting COD calls checkout-payment record_cod and fires the same session.completed webhook with payment_method: "cod".

Configure webhooks

Set up signed webhook delivery for checkout lifecycle events.

Webhooks & events