DEV Community

Cover image for Razorpay and Stripe in one FastAPI app, without trusting the browser
Aravind Govindhasamy
Aravind Govindhasamy

Posted on Originally published at starter.budeglobal.in on

Razorpay and Stripe in one FastAPI app, without trusting the browser

If you sell to customers in India, you want UPI, and for that you want Razorpay. If you also sell abroad, Stripe Checkout is the easiest way to take cards. The starter does both from one FastAPI app, with one rule that matters more than either provider: the browser never decides that a payment succeeded.

Prices live on the server

Products and their prices are a dictionary in backend/app/api/payments.py:

PRODUCTS: dict[str, dict] = {
    "pro": {
        "name": "Pro upgrade",
        "plan": "pro", # profiles.plan after payment
        "prices": {"stripe": (1900, "usd"), "razorpay": (149900, "inr")},
    },
}

Enter fullscreen mode Exit fullscreen mode

Amounts are in minor units (cents and paise). The client sends a product id, never an amount, so nobody can buy the Pro plan for one rupee by editing a request.

Pending until a webhook says otherwise

Checkout creates a pending row in payments and returns the provider's checkout URL or order. When the customer pays, the provider calls a webhook:

  • Stripe: /payments/webhooks/stripe, with the checkout.session.* events
  • Razorpay: /payments/webhooks/razorpay, with order.paid and payment.failed

The backend verifies the signature, and only then marks the payment paid and calls on_paid(), which upgrades the user's plan. A success page in the browser is just a page; it changes nothing.

Exactly once, even when providers retry

Payment providers retry webhooks, sometimes several times. Each event is recorded with its id, and applying it twice is a no-op, so a retried order.paid can't grant the plan twice or notify the buyer twice. The tests replay webhooks to prove it.

Money is never added across currencies

Revenue charts and the admin payments list keep each currency separate. Adding ₹499 to $9 gives a number that means nothing, so the starter never does it.

Turning it on

A provider appears in the UI once its keys are in .env. Set the webhook URL in the provider's dashboard, pick the events above, and you can take a test payment in a few minutes. The payments guide has the details.

Top comments (0)