Razorpay API keys and webhooks: from test mode to live payments
Razorpay is the easiest way to accept UPI, cards and netbanking in India. This guide gets you from a new account to verified payments: keys, an order created on your server, Razorpay Checkout in the browser, and a webhoo
Razorpay is the easiest way to accept UPI, cards and netbanking in India. This guide gets you from a new account to verified payments: keys, an order created on your server, Razorpay Checkout in the browser, and a webhook that confirms the payment.
How the pieces fit
- Your API creates an order with the amount (server-side, so the browser can't change the price).
- The browser opens Razorpay Checkout with that order's id. The customer pays by UPI, card or netbanking.
- Razorpay calls your webhook with
order.paid. Your API checks the signature and marks the payment paid.
The browser's "payment succeeded" callback is only a hint for the UI. The signed webhook is the only thing you should trust.
1. Generate test keys
In the Razorpay Dashboard, switch to Test mode , then go to Account & Settings β API Keys (under Website and app settings ) and click Generate Key.
You get a Key ID (starts rzp_test_, safe to send to the browser) and a Key Secret (server only). The dashboard won't show the secret again, so store it right away.
RAZORPAY_KEY_ID=rzp_test_...
RAZORPAY_KEY_SECRET=...
Test mode is a full simulation: no real money moves, and Razorpay provides test cards and UPI ids.
2. Create an order from FastAPI
Amounts are in paise: βΉ1,499.00 is 149900.
import httpx
def create_order(amount_paise: int, receipt: str) -> dict:
r = httpx.post(
"https://api.razorpay.com/v1/orders",
auth=(RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET),
json={"amount": amount_paise, "currency": "INR", "receipt": receipt},
timeout=15,
)
r.raise_for_status()
return r.json() # contains "id": "order_..."
Look the price up on the server from a product id. Never accept an amount from the browser.
3. Open Checkout in the browser
Load Razorpay's script and open Checkout with your Key ID and the order id:
await new Promise((resolve) => {
const s = document.createElement("script")
s.src = "https://checkout.razorpay.com/v1/checkout.js"
s.onload = resolve
document.body.append(s)
})
new (window as any).Razorpay({
key: RAZORPAY_KEY_ID,
order_id: order.id,
name: "Example",
handler: () => showMessage("Payment received, confirmingβ¦"),
}).open()
4. Add the webhook
Webhooks need a public HTTPS URL; Razorpay won't save a localhost one. For local testing, expose your API with a tunnel such as Cloudflare Tunnel or ngrok.
In the Dashboard (in the same mode as your keys), go to Account & Settings β Webhooks β Add New Webhook :
-
Webhook URL:
https://api.example.com/payments/webhooks/razorpay - Secret: a long random string you generate; it's not your Key Secret
- Alert Email: where Razorpay reports failures
-
Active Events:
order.paidandpayment.failed
RAZORPAY_WEBHOOK_SECRET=...
Set the webhook up again when you switch to live mode, with its own secret.
5. Verify the signature
Razorpay signs the raw request body with HMAC-SHA256 using the webhook secret and sends the hex digest in X-Razorpay-Signature:
import hashlib
import hmac
import json
from fastapi import HTTPException, Request
@app.post("/payments/webhooks/razorpay")
async def razorpay_webhook(request: Request):
body = await request.body() # the raw bytes, before any JSON parsing
expected = hmac.new(RAZORPAY_WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("x-razorpay-signature", "")):
raise HTTPException(400, "Bad signature")
event = json.loads(body)
if event["event"] == "order.paid":
mark_paid(event["payload"]["order"]["entity"]["id"]) # idempotent
return {}
Razorpay retries deliveries, so make mark_paid idempotent: record each event or order id once, and ignore repeats.
6. Go live
- In live mode, Razorpay asks for the website where you'll take payments and verifies it (it can take a few working days). Make sure the site has the policy pages Razorpay checks for: terms, privacy, refunds and cancellation, shipping or delivery, and contact details.
- Generate live keys (
rzp_live_...) and a live webhook with a new secret. - Swap the three values in production and make a small real payment to check.
Using it with FastAPI Supabase Starter
Steps 2, 3 and 5 are already built: products and prices live on the server, the billing page opens Checkout, and the webhook verifies the signature and applies each event exactly once before upgrading the user's plan. Set the three RAZORPAY_* values, point the webhook at /payments/webhooks/razorpay, and the public site already has the policy pages Razorpay asks for. See Payments and the payments design.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.