Verification Checklist
End-to-end verification of your Razorpay integration — from account setup through a successful test payment and database confirmation.
Work through this checklist in order. Each step builds on the previous one. Do not move to the next item until the current one passes — skipping ahead makes bugs harder to locate.
Phase 1: Account and Configuration
Complete these before writing any code. Configuration issues are harder to debug when code is already involved.
Razorpay Account
- Razorpay account created at razorpay.com
- Email verified (confirmation link clicked)
- Mobile verified (OTP entered)
- Logged into dashboard — landing on the Home page
- Mode confirmed: Test Mode toggle is active (blue indicator, top right)
- Merchant ID noted from Settings → Profile
API Keys
- Settings → API Keys → "Generate Test Key" clicked
- Key ID copied (starts with
rzp_test_) - Key Secret copied (shown only once — do this now)
- Both keys saved to VaultMate under the project name
- Key ID added to
.env.localasNEXT_PUBLIC_RAZORPAY_KEY_ID - Key Secret NOT in
.env.local(it should not appear in this file) - Supabase secret set:
supabase secrets set RAZORPAY_KEY_ID=rzp_test_xxx - Supabase secret set:
supabase secrets set RAZORPAY_KEY_SECRET=xxx - Verification:
supabase secrets listshows both keys (values masked — this is correct)
.gitignore Check
-
.env.localis listed in.gitignore -
git statusdoes not show.env.localas a file to be committed
Phase 2: Edge Function Deployment
Create Payment Order
-
supabase/functions/create-payment-order/index.tsexists - Function reads
RAZORPAY_KEY_IDandRAZORPAY_KEY_SECRETfromDeno.env.get() - Amount is read from the database, not from the request body
- Function inserts a record into
payment_transactionstable withstatus: 'pending' - Function deployed:
supabase functions deploy create-payment-order - Deployment confirmed:
supabase functions listshows the function
Verify Payment
-
supabase/functions/verify-payment/index.tsexists - HMAC-SHA256 signature verification is implemented
- Function updates
payment_transactionstostatus: 'completed' - Function updates
fee_recordstostatus: 'paid' - Function deployed:
supabase functions deploy verify-payment
Webhook (if configured)
-
supabase/functions/razorpay-webhook/index.tsexists - Webhook secret generated and saved to VaultMate
- Supabase secret set:
supabase secrets set RAZORPAY_WEBHOOK_SECRET=xxx - Webhook registered in Razorpay Dashboard → Settings → Webhooks
- Webhook URL matches the deployed edge function URL exactly
-
payment.capturedandpayment.failedevents are subscribed - Function deployed:
supabase functions deploy razorpay-webhook
Phase 3: Frontend Integration
- Razorpay Checkout SDK loaded:
<Script src="https://checkout.razorpay.com/v1/checkout.js" />in layout -
rupeesToPaise()helper function defined insrc/lib/razorpay.ts -
openRazorpayModal()function usesprocess.env.NEXT_PUBLIC_RAZORPAY_KEY_ID(not hardcoded) -
PaymentButtoncomponent callscreate-payment-orderedge function viasupabase.functions.invoke -
handlercallback callsverify-paymentedge function with all three Razorpay IDs -
modal.ondismisscallback is set to handle user cancellation
Phase 4: End-to-End Test Payment
This is the final test. Run through the complete flow as a real customer would.
Before Running the Test
- A test fee record exists in your database with
status: 'pending' - The fee record has a real amount (e.g., ₹2,500 — a monthly tuition fee)
- You are logged in as a test parent in the app
- Browser console is open (F12 → Console tab)
- Razorpay Dashboard → Orders tab is open in another tab
Executing the Test Payment
- Click the Pay Now button on the fee payment screen
- Observe the Razorpay modal opens over your page
- Select Credit/Debit Card as the payment method
- Enter the test card:
- Card number:
4111 1111 1111 1111 - Expiry: any future date (e.g., 12/29)
- CVV: any 3 digits (e.g., 123)
- Name: any name
- Card number:
- Click Pay
- If prompted for OTP: enter any 6 digits (e.g., 123456)
- Payment should complete — modal closes
Verification Points After the Test Payment
In Razorpay Dashboard:
- Dashboard → Orders: a new order appears with your fee record ID as the receipt
- Dashboard → Transactions: a payment with status "Captured" appears
- The payment amount matches the fee amount from the database (check it is ₹2,500, not ₹25.00)
- The payment ID (starts with
pay_) is visible
In your database:
Expected result in payment_transactions:
- A row with
status: 'completed' -
provider_payment_idis populated (not null) -
amountmatches the fee amount fromfee_structures
Expected result in fee_records:
- Your test fee record appears with
status: 'paid' -
payment_idmatches the Razorpay payment ID -
paid_atis populated
In your app:
- The fee payment confirmation screen is shown to the user
- No error messages appear
- Browser console shows no errors
Phase 5: Failure Scenario Test
Test that failures are handled correctly too.
- Click Pay Now on another test fee record
- In the Razorpay modal, enter the failure test card:
4000 0000 0000 0002 - Expiry: any future date, CVV: any 3 digits
- Click Pay
Expected behaviour:
- Razorpay modal shows a payment failure message
- Your app's
onFailurecallback fires - Parent sees an appropriate error message (not a white screen or spinner stuck forever)
- Fee record status in database remains
pending(not corrupted)
Phase 6: UPI Test
- Click Pay Now on another test fee record
- In the Razorpay modal, select UPI
- Enter
success@razorpayas the UPI ID - Click Pay
Expected behaviour:
- Razorpay simulates a UPI payment request
- Payment completes successfully
- Fee record status updates to
paidin database
Then repeat with failure@razorpay to test the failure path.
Common Issues and Fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Modal does not open | checkout.js not loaded | Confirm Script tag is in layout, check network tab |
| "Invalid key" error from Razorpay | Wrong key format or mode mismatch | Check NEXT_PUBLIC_RAZORPAY_KEY_ID in .env.local starts with rzp_test_ |
| Order created but amount shows ₹2.99 for ₹299 | Amount in rupees, not paise | All amounts to Razorpay must be multiplied by 100 |
| Payment completes but fee record still shows pending | Verify edge function not called or failed | Check Supabase logs: supabase functions logs verify-payment |
| "Unauthorized" from edge function | Auth header not passed | Confirm supabase.functions.invoke is called with auth context |
| Signature verification fails | Mismatched key or wrong string format | The string to hash is `orderId |
| No orders in Razorpay dashboard | Looking at Live dashboard while testing | Toggle to Test Mode in Razorpay dashboard top right |
| Webhook not receiving events | Wrong URL or function not deployed | Razorpay → Settings → Webhooks → View Attempts for details |
Reconciliation Spot Check
Once the test payment passes, do this three-way reconciliation as a CA would:
| Check | Razorpay | payment_transactions table | fee_records table |
|---|---|---|---|
| Payment ID | pay_xyz789 | provider_payment_id = pay_xyz789 | payment_id = pay_xyz789 |
| Amount | ₹2,500.00 | amount = 2500.00 | Matches amount in fee_structures |
| Status | Captured | status = completed | status = paid |
| Timestamp | Within seconds of each other | updated_at matches | paid_at matches |
All four values align across all three sources. This is what a clean payment implementation looks like.
You Are Done When
- Test payment with a card completes end-to-end
- Test payment with UPI (
success@razorpay) completes end-to-end - Failure scenario leaves fee record in the correct
pendingstate - All three database checks pass (payment_transactions + fee_records + Razorpay dashboard)
- No hardcoded keys anywhere in the codebase (
git grep rzp_test_returns nothing) - Both API keys are in VaultMate
At this point your Razorpay integration is complete and production-ready (pending Live Mode activation when you go live with a real client).