@razorpay/razorpay-js
v1.0.0
Published
Official npm loader for Razorpay's browser products — Standard Checkout, Custom Checkout, and RazorpayID
Readme
@razorpay/razorpay-js
Official npm loader for Razorpay's browser products — Standard Checkout,
Custom Checkout, and RazorpayId — replacing hand-rolled
<script src="https://checkout.razorpay.com/v1/..."> injection.
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
const checkout = await RazorpayCheckout({
key: 'rzp_test_xxx',
order_id: 'order_xxx',
});
checkout.open();Importing the product also starts downloading its script, so by the time a
customer reaches your pay button the await is instant. If they get there
first, the await simply waits. There is nothing else to wire up — no
readiness flag, no load callback, no stored constructor.
What you get back is a real product instance: the exact object a
<script> tag would have produced. Nothing is proxied and no contract is
altered.
Not the server-side SDK. This package runs in the browser. For server-side API calls (orders, refunds, payouts) use the
razorpayNode package.
Why this package?
- No
<script>tag. Import it like any other npm dependency; scripts are injected for you, at most once per page. - Multiple products, one page. Historically every Razorpay script claimed
the
window.Razorpayglobal — last one loaded won, so Standard and Custom Checkout could not coexist. Each product function resolves its own product (see How it works). - Failures you can act on. A blocked or failed script rejects your
await, at the click, where you can still show a fallback. - Types included, and an out-of-date type can never break your build.
- Zero dependencies, a few kilobytes, ESM and CJS.
Install
npm install @razorpay/razorpay-jsIntegration styles
There are two, plus one optimisation. The code at the payment moment is identical in all of them — only the moment the download starts differs, so there is no way to wire this up incorrectly, only faster or slower.
Default — import the product where you use it
Recommended.
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
// the script is already downloading
async function payNow(order) {
const checkout = await RazorpayCheckout({
key: 'rzp_live_xxx',
order_id: order.id,
handler: (response) => verifyOnServer(response),
});
checkout.open();
}Add a pre-load at your app entry
If your payment code sits in a lazily-loaded route, the import above only runs when that route runs. A bare import at your entry file starts the download at boot instead. Nothing else changes.
// src/main.ts
import '@razorpay/razorpay-js/checkout';Deferred — nothing loads until you say so
Import from the package root instead. Nothing touches the network until you
either call loadRazorpay or await a product.
import { loadRazorpay, RazorpayCheckout } from '@razorpay/razorpay-js';
function onCartOpened() {
loadRazorpay('checkout'); // optional: start the download now
}
const checkout = await RazorpayCheckout({ key, order_id, handler });
checkout.open();Products
| Product | Import | Slug (for loadRazorpay) |
| ----------------- | --------------------------------------- | ------------------------- |
| Standard Checkout | @razorpay/razorpay-js/checkout | checkout |
| Custom Checkout | @razorpay/razorpay-js/custom-checkout | custom_checkout |
| Magic Checkout | @razorpay/razorpay-js/magic-checkout | magic_checkout |
| RazorpayId | @razorpay/razorpay-js/id | id |
Standard Checkout
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
const checkout = await RazorpayCheckout({
key: 'rzp_live_xxx',
order_id: order.id,
amount: order.amount,
currency: 'INR',
handler: (response) => verifyOnServer(response),
});
checkout.on('payment.failed', (e) => showError(e.error.description));
checkout.open();Magic Checkout
Magic is its own product, not Standard Checkout with an extra option — it is
served by a different script, and only that script marks the checkout iframe as
Magic and ships the <magic-checkout-btn> element. Import it and the rest is
Standard Checkout's API.
import RazorpayMagicCheckout from '@razorpay/razorpay-js/magic-checkout';
const checkout = await RazorpayMagicCheckout({
key: 'rzp_live_xxx',
order_id: order.id,
});
checkout.open();Whether the one-click flow actually runs is decided by your Razorpay preferences and the order — importing this product is what makes it reachable.
Custom Checkout
import RazorpayCustomCheckout from '@razorpay/razorpay-js/custom-checkout';
const rzp = await RazorpayCustomCheckout({ key: 'rzp_live_xxx' });
rzp.on('payment.success', (r) => verifyOnServer(r));
rzp.on('payment.error', (e) => showError(e.error.description));
rzp.createPayment({ amount, currency: 'INR', order_id, method: 'upi', vpa });Custom Checkout also exposes methods on the constructor, for merchants who need the available payment methods before any payment exists. These are available only after the product has loaded — await anything for this product first:
import RazorpayCustomCheckout from '@razorpay/razorpay-js/custom-checkout';
import { loadRazorpay } from '@razorpay/razorpay-js';
await loadRazorpay('custom_checkout');
RazorpayCustomCheckout.payment.getMethods(renderOptions);Using them earlier throws a product_not_loaded error naming the fix. Note
that on a warm cache the script is usually there already, so code that skips
the await can pass local testing and fail for a first-time customer on a
slow connection — keep the awaiting line even where it looks redundant.
RazorpayId
RazorpayId reports some failures as return values rather than exceptions. The loader passes whatever the product returns through untouched.
import RazorpayId from '@razorpay/razorpay-js/id';
const id = await RazorpayId({ key: 'rzp_live_xxx', mode: 'widget' });
if ('code' in id) {
fallbackToOwnLogin(id); // { code, description, reason, source }
} else {
id.on('login.success', (user) => onLoggedIn(user));
id.open();
}Several products on one page
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
import RazorpayId from '@razorpay/razorpay-js/id';
// both download independently, in parallel
const id = await RazorpayId({ key, mode: 'headless' });
const checkout = await RazorpayCheckout({ key, order_id });React / Next.js
No useEffect, no readiness state, no stored constructor.
'use client';
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
export function PayButton({ order }) {
const [busy, setBusy] = useState(false);
const pay = async () => {
setBusy(true);
try {
const checkout = await RazorpayCheckout({
key: 'rzp_live_xxx',
order_id: order.id,
handler: onSuccess,
});
checkout.open();
} catch {
redirectToHostedCheckout(order.id);
} finally {
setBusy(false);
}
};
return (
<button onClick={pay} disabled={busy}>
Pay
</button>
);
}API
RazorpayCheckout(options) · RazorpayCustomCheckout(options) · RazorpayMagicCheckout(options) · RazorpayId(options)
Returns a promise for a ready product instance. options is the product's own
constructor options, unchanged.
Guarantees:
- Correct whenever you call it. It resolves immediately if the product is already there, waits if a load is in flight, reuses a script tag already on the page, or starts a load — you never have to know which.
- At most one script per page, however many components import or call it.
- A new instance per call, built from that call's options. (A Checkout instance belongs to one payment attempt, so reusing one across orders would silently open the wrong order.)
- Failures are never cached — the next call retries, so a customer who hit a flaky network succeeds when they click again.
- It never hangs: every load is internally time-bounded and always settles.
Need one shared instance? Make one — with your options, by your choice:
export const rzpPromise = RazorpayCustomCheckout({ key: 'rzp_live_xxx' });
// elsewhere
const rzp = await rzpPromise;loadRazorpay(slug | slug[], options?)
Optional. Starts one or more products loading, so you control when the download happens. It affects speed, never correctness.
loadRazorpay('checkout'); // fire and forget
await loadRazorpay(['checkout', 'id']); // or wait for both- Resolves to nothing — products come from their own functions.
- Safe to ignore: an ignored failure never becomes an unhandled rejection, and
reappears at the product's
await, where you can handle it. - With several slugs it rejects with the first failure; the others carry on. A
later failure is not lost — it surfaces the next time that product is
awaited. For one result per product, use
Promise.allSettled([loadRazorpay('checkout'), loadRazorpay('id')]). - Passing an unknown slug throws immediately — that is a mistake in your code, not a runtime condition, so it is never swallowed by a fire-and-forget call.
Options — nonce: a CSP nonce for the injected script. Only needed when
your Content-Security-Policy is nonce-based and neither allows
checkout.razorpay.com nor uses 'strict-dynamic'. The loader detects your
page's existing nonce automatically, so this is an override rather than a
requirement.
Errors
Every rejection from the loader is a RazorpayLoaderError following
Razorpay's standard error format. Branch on reason; description is
human-readable and may change.
import RazorpayCheckout from '@razorpay/razorpay-js/checkout';
import { RazorpayLoaderError, ERROR_REASONS } from '@razorpay/razorpay-js';
try {
const checkout = await RazorpayCheckout(options);
checkout.open();
} catch (error) {
if (error instanceof RazorpayLoaderError) {
redirectToHostedCheckout(order.id); // blocked, offline, or timed out
}
}| reason | Meaning | What to do |
| ------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------- |
| script_load_failed | Network error, content blocker, CSP block, or a CDN error. The most common real failure. | Retriable. Show a fallback. |
| script_load_timeout | The script did not arrive in time. | Retriable. |
| product_registration_missing | The script loaded but the product was not there afterwards. | Not fixable by you — contact support. |
| product_not_loaded | A constructor method was used before the product loaded. | Await the product first. |
| unknown_product | Unknown slug passed to loadRazorpay. | Fix the slug. |
| input_validation_failed | Bad argument. | Fix the call. |
| unsupported_environment | Called outside a browser (typically SSR). | Move the call to client-side code. |
Anything the product itself throws or returns passes through untouched — so a
RazorpayLoaderError always means the loader could not deliver the product,
and anything else is the product speaking.
Server-side rendering
Every import is inert on the server, including the product paths, so importing
this package never breaks a server render. Awaiting a product outside a
browser rejects with unsupported_environment — in practice this does not
happen, because awaits live in event handlers.
TypeScript
Types ship with the package; each entry point carries its own, so importing
/checkout gives you Checkout's types only.
Product types are deliberately loose: verified members are typed strictly (you get autocompletion and signature checks), and anything else is admitted through an index signature. An out-of-date type in this package can never break your build — at worst a newly shipped product method lacks autocompletion until its type lands here. Products deploy independently of this package, so this is on purpose.
How it works
The package contains no product logic. Awaiting a product function:
- looks for that product's constructor already on the page;
- injects its evergreen CDN script if nothing is loading yet, or joins the load already running, or reuses a script tag already in the DOM;
- constructs with the product's real constructor and hands you the result, untouched.
Products publish themselves either under a global named for exactly one
product (window.RazorpayId) or, when they share window.Razorpay, under
their slug in window.Razorpay._modules. That is what lets two products
coexist on a page without one overwriting the other. Existing script-tag
integrations are unaffected — window.Razorpay behaves exactly as before.
Support
- Docs: https://razorpay.com/docs
- Support: https://razorpay.com/support/
