Razorpay checkout modal opened and payment appeared to complete, but the webhook was never fired and the subscription wasn't activated. Root cause: client-side key was in test mode (rzp_test_) while the server-side Cloud Function key was in live mode (rzp_live_), or vice versa. Both keys must match modes simultaneously.
Resolution Steps
During TrustSeal's Razorpay integration phase (February 2026), the first end-to-end payment test appeared to work: the checkout modal opened, the test card was entered, the payment succeeded on the Razorpay modal, and the modal closed. But the TrustSeal dashboard didn't activate the premium tier. The Firestore subscription document was never created.
The symptom looked like a webhook configuration issue. The Razorpay dashboard showed the test transaction under Test → Payments, but the webhook logs (Test → Webhooks → Recent Events) had no entries for the event.
The actual cause: the client-side checkout used rzp_test_ key, but the Cloud Function that processed the webhook was configured with the rzp_live_ key. Razorpay's test and live environments are completely isolated. A test-mode payment generates a test webhook — but the live-mode server endpoint was never registered to receive test webhooks, so nothing arrived.
Razorpay's two environments are separated by key prefix only. There is no obvious error. The checkout modal works in both modes. Payments complete in both modes. The differentiation is only visible in:
rzp_test_ vs rzp_live_)A developer who switches the server-side key to live mode while leaving the client-side key in test mode (or vice versa) gets a completely functional-looking checkout with no payment record on the other side.
| Context | Key Type | Prefix |
|---|---|---|
| Client-side checkout (browser) | key_id | rzp_test_ or rzp_live_ |
| Server-side API calls | API key + secret | Matching prefix |
| Webhooks received | Environment-scoped | Separate test/live endpoints |
Rule: Both keys must always have the same prefix. There is no cross-mode communication.
Razorpay requires both the client-side key ID and the server-side secret key to operate in the same mode. The mode is determined by the key prefix — rzp_test_ for test mode, rzp_live_ for production.
When the TrustSeal server was migrated from test to live mode keys (for the Razorpay production go-live on 2026-03-10), the Cloud Function environment variable was updated but the client-side key in the React build was not updated simultaneously. Result: the client was still sending transactions to the test environment, while the server was configured to receive live webhooks only.
Update both keys to use the same mode prefix. In TrustSeal's case, the go-live sequence was:
VITE_RAZORPAY_KEY_ID in the Vite build to rzp_live_*RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET in Firebase Functions config to live mode valuesfirebase deploy --only functionsnpm run build && cd dist && git push (new client build with live key)After this, both the client-side checkout and the server-side webhook handler operated in live mode. The webhook fired correctly and the Firestore subscription record was created.
Pre-deployment checklist for Razorpay mode switch:
Operational pattern: The test/live key mismatch is a subcategory of the runtime-environment-scope-drift pattern — credentials scoped to one environment are being used by a system expecting another environment's credentials. The same class of failure applies to Firebase API keys, Stripe keys, SendGrid keys, and any service with environment isolation.
Fix Confidence
Recovery Complexity
Pattern Family
This failure belongs to a named recurring pattern. Other failures in this family share the same root cause structure — understanding the pattern prevents multiple failure types simultaneously.
Related Failures in Same Pattern
Prevention Lessons
Completing these lessons would have prevented this failure.
Demonstrated In
This failure occurred in a real production context. These case studies show the full arc from incident to resolution.