Accept payments with Razorpay in DaloyJS
Razorpay is the default payment stack for India, UPI, cards, netbanking, wallets, EMI, and BNPL through one API. This guide uses the official razorpay Node SDK with the Orders flow, the SDK's built-in validatePaymentVerification for the post-checkout callback, and validateWebhookSignature for IPN.
What you should know up front
- Two signatures, not one. Razorpay signs two different things: (1) the client-side checkout result, verify with
validatePaymentVerificationusing your key secret; (2) the server-side webhook, verify withvalidateWebhookSignatureusing your webhook secret. They're different secrets and different payloads. - Orders are the source of truth, not raw payments. Create an Order on your server, hand
order_idto Checkout, then verify the callback. Skipping the Order step is technically allowed but loses you idempotency, reconciliation, and the "late authorisation" protection. - Amounts are paise. ₹500.00 →
{ amount: 50000, currency: "INR" }. Don't pass floats, the API rejects them. - Webhook verification needs the raw body.
JSON.parse+JSON.stringifychanges byte order, which breaks the HMAC. Read the request body as a string before parsing it. - Don't roll your own HMAC. The SDK exposes both verifiers as plain helpers; using them keeps you aligned when Razorpay tweaks the algorithm or adds new fields.
1. Provision
- Sign in to the Razorpay dashboard.
- Account & Settings → API Keys → Generate Test Key. Save the
key_idandkey_secret: the secret is shown once. - Account & Settings → Webhooks → Add new. Point it at your DaloyJS endpoint and set a webhook secret. Subscribe to at least
payment.captured,payment.failed,order.paid, andrefund.processed. - Activate the methods you need (UPI/cards are on by default; netbanking and wallets typically need explicit enabling).
2. Install
3. Environment variables
4. Plugin
The verifier helpers live at razorpay/dist/utils/razorpay-utilsin the published bundle, Razorpay's own README points there. They're plain functions over node:crypto; no SDK instance needed.
5. Create an order
The frontend uses Razorpay Checkout JS with the orderId from this endpoint plus your public key_id.
6. Verify the client callback
- 01requestBrowserDaloyJS routePOST /checkout/razorpay/orderamount in paise + receipt
- 02requestDaloyJS routeRazorpayorders.create, return order_id + key_idCheckout JS opens with order_id
- 03requestBrowserDaloyJS routePOST /verify after Checkout successrazorpay_order_id, _payment_id, _signature
- 04noteDaloyJS routeDaloyJS routevalidatePaymentVerification with the key secret401 on mismatch, then fetchPayment
- 05responseDaloyJS routeBrowser200 only when payment.status is 'captured'signature proves origin, not capture
After a successful payment, Checkout JS posts { razorpay_order_id, razorpay_payment_id, razorpay_signature } back to your client. Forward to the server and verify before doing anything:
7. Webhook
- 01requestRazorpayDaloyJS routePOST /webhooks/razorpayX-Razorpay-Signature over raw body
- 02noteDaloyJS routeDaloyJS routevalidateWebhookSignature with the webhook secretdifferent secret from the client callback
- 03responseDaloyJS routeRazorpay401 when the signature does not match{ error: 'bad signature' }
- 04asyncDaloyJS routeYour queueHandle payment.captured / refund.processed, then ack200 even for unhandled events
Always return 200 once the signature checks out, even for events you don't handle. Razorpay retries non-2xx responses with exponential backoff for up to 24 hours.
8. Refunds
Runtimes
The razorpay SDK ships CJS and depends on Node's https module, it runs on Node 18+ but is not edge-runtime compatible. For Cloudflare Workers, hit https://api.razorpay.com/v1 directly with fetch and Basic auth (Authorization: Basic base64(key_id:key_secret)). The two signature helpers are pure HMAC and easy to reimplement with crypto.subtleif you don't want the bundled ones.
Errors
Razorpay throws errors with a structured error.error object containing code, description, field, and reason. Map them through problem+json with the Razorpay code on the type field so reconciliation tools can match them later.
Modernisation notes
- Use Orders, not bare payment links. The Orders flow gives you a server-side anchor for idempotency, lets Checkout JS show the right amount, and unlocks
order.paidwebhooks that fire even when the customer closes the tab before the success callback. - Verify both signatures. The client callback signature stops forged success posts from the browser; the webhook signature stops spoofed IPNs. Skipping either is a foot-gun.
- Don't fulfil on the client callback alone. The signature proves the call came from Razorpay, but
status: createdisn'tcaptured. Always re-fetch the payment (or wait for the webhook) before flipping an order to paid. - Use Promises, ignore the callback API. Every method on the SDK returns a Promise. The error-first callback parameter still works but exists for legacy code only.
See also the payments overview, Tap Payments guide, PayTabs guide, and problem+json errors.