npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payment-universal

v0.1.1

Published

Framework-agnostic multi-gateway payment SDK — Razorpay, Cashfree, PayU, Juspay, Stripe — with React, Vue 3, Angular, and Vanilla adapters.

Readme

payment-universal

npm license types bundle

A framework-agnostic TypeScript SDK for Razorpay, Cashfree, PayU, Juspay, and Stripe. One API, swappable adapters, first-class bindings for React, Vue 3, Angular, and Vanilla JS.

  • Landing page: https://payment-universal.vercel.app/
  • Source: https://github.com/Rupam-Shil/payment-universal
  • Issues: https://github.com/Rupam-Shil/payment-universal/issues

Status: pre-release. 0.1.x is honest about being early. The architecture is stable, 61 unit tests are green across all 5 gateways × browser/server and all 4 framework adapters, but sandbox smoke tests against live gateway APIs are still in progress. Expect a handful of breaking changes before 1.0.


Why this exists

Picking a payment gateway is a business decision that changes. razorpay-universal (this project's ancestor) handled Razorpay only. payment-universal takes the same framework-integration discipline and applies it to five gateways behind a uniform API.

The promise: swap two import lines — one on the client, one on the server — and the rest of your integration stays identical. The framework hook, the normalized order shape, the verify endpoint, the result handling: unchanged.

The abstraction has limits, and that's the whole point. Gateways genuinely differ — PayU, Juspay, and Stripe have no drop-in modal in v1, only hosted redirects. Rather than pretending otherwise, payment-universal surfaces capability flags on every adapter and throws UnsupportedModeError synchronously when you ask for something a gateway can't do. Fail at the call site, not mid-transaction.

Highlights

  • 5 gateways × 2 sides × 4 frameworks, all tree-shakable per gateway/framework.
  • Two-tier adapter split — server entries use the "node" export condition so browser bundlers physically cannot import code that holds your secret keys.
  • Gateway-agnostic framework hooksuseCheckout(adapter) doesn't care which gateway you passed in.
  • Normalized typesOrderRequest, NormalizedOrder, PaymentResult, VerificationResult are identical across gateways. The gateway-specific bits live in clientPayload / raw.
  • Promise-based open() wrapping each gateway's native callback/event SDK. Modal dismissal rejects with CheckoutDismissedError. Payment failure rejects with PaymentError.
  • SSR-safe — every window / document access is guarded. Angular uses PLATFORM_ID. React does no work outside useEffect.
  • Zero runtime dependencies. Every framework and browser SDK is an optional peer dep.

Capability matrix (v1)

| Gateway | Modal | Redirect | Script host | Optional browser peer dep | |-----------|:-----:|:--------:|--------------------------|------------------------------------------| | Razorpay | ✅ | ✅ | checkout.razorpay.com | — | | Cashfree | ✅ | ✅ | sdk.cashfree.com | @cashfreepayments/cashfree-js | | PayU | ❌ | ✅ | (server-generated form) | — | | Juspay | ❌ | ✅ | (server-generated URL) | — | | Stripe | ❌ | ✅ | js.stripe.com | @stripe/stripe-js |

Webhooks, subscriptions, and refunds are false for every gateway in v1 — on the v2 roadmap.

Install

npm install payment-universal
# or
pnpm add payment-universal
# or
yarn add payment-universal

Optional peer deps — install only for gateways and frameworks you actually use:

# Framework peers
npm install react                                    # React adapter
npm install vue                                      # Vue 3 adapter
npm install @angular/core @angular/common rxjs       # Angular adapter
# (Vanilla needs nothing extra)

# Gateway browser SDKs
npm install @cashfreepayments/cashfree-js            # Cashfree modal / redirect
npm install @stripe/stripe-js                        # Stripe redirectToCheckout

Razorpay, PayU, and Juspay don't need a separate browser SDK package — the loader injects their scripts directly, or (PayU/Juspay) the server generates a redirect target.

Quick start (React + Razorpay + Next.js)

The shortest path to a working checkout.

1. Server: create an order

Server entries live at payment-universal/{gateway}/server — these are Node-only and hold your secret key.

// app/api/checkout/route.ts
import { razorpayServer } from 'payment-universal/razorpay/server';

const server = razorpayServer({
  keyId: process.env.RAZORPAY_KEY_ID!,
  keySecret: process.env.RAZORPAY_KEY_SECRET!,
});

export async function POST(req: Request): Promise<Response> {
  const { amount, currency = 'INR' } = await req.json();
  const order = await server.createOrder({
    amount,
    currency,
    receipt: `rcpt_${Date.now()}`,
  });
  return Response.json(order); // NormalizedOrder
}

2. Client: open the checkout

// components/PayButton.tsx
'use client';
import { useCheckout } from 'payment-universal/react';
import { razorpayBrowser } from 'payment-universal/razorpay/browser';

const adapter = razorpayBrowser({
  keyId: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
});

export function PayButton() {
  const { open, isReady, isLoading, error } = useCheckout(adapter);

  async function handlePay() {
    const order = await fetch('/api/checkout', {
      method: 'POST',
      body: JSON.stringify({ amount: 49900 }),
    }).then((r) => r.json());

    try {
      const result = await open({
        order,
        mode: 'modal',
        prefill: { email: '[email protected]', name: 'Jane' },
      });
      await fetch('/api/verify', {
        method: 'POST',
        body: JSON.stringify(result),
      });
    } catch (err) {
      // CheckoutDismissedError, PaymentError, etc.
      console.error(err);
    }
  }

  if (error) return <p>Failed to load checkout: {error.message}</p>;
  return (
    <button disabled={!isReady || isLoading} onClick={handlePay}>
      Pay ₹499
    </button>
  );
}

3. Server: verify the payment

// app/api/verify/route.ts
import { razorpayServer } from 'payment-universal/razorpay/server';

const server = razorpayServer({
  keyId: process.env.RAZORPAY_KEY_ID!,
  keySecret: process.env.RAZORPAY_KEY_SECRET!,
});

export async function POST(req: Request): Promise<Response> {
  const paymentResult = await req.json(); // from the browser
  const verification = await server.verifyPayment(paymentResult);
  // verification: { verified: true, orderId, paymentId, amount, status: 'paid', gateway }

  // Always cross-check verification.amount against your own DB record before
  // fulfilling — the library tells you the gateway says it's paid; you decide
  // whether that matches what you expected.

  return Response.json(verification);
}

That's the entire loop.

Switching gateways

The whole promise, in one diff:

- import { razorpayBrowser } from 'payment-universal/razorpay/browser';
- const adapter = razorpayBrowser({ keyId: process.env.NEXT_PUBLIC_RZP_KEY! });
+ import { cashfreeBrowser } from 'payment-universal/cashfree/browser';
+ const adapter = cashfreeBrowser({ appId: process.env.NEXT_PUBLIC_CF_APP!, mode: 'production' });
- import { razorpayServer } from 'payment-universal/razorpay/server';
- const server = razorpayServer({ keyId, keySecret });
+ import { cashfreeServer } from 'payment-universal/cashfree/server';
+ const server = cashfreeServer({ appId, secretKey });

The useCheckout(adapter) call, the open({ order, mode, prefill }) call, the server.createOrder(req) call, and the server.verifyPayment(payload) call are all unchanged. If the new gateway doesn't support the mode you asked for, you get UnsupportedModeError synchronously at open() — visible at runtime via adapter.capabilities if you want to check before calling.

Framework adapters

The framework hooks are gateway-agnostic — they consume the BrowserAdapter interface and don't reference any specific gateway.

React

import { useCheckout } from 'payment-universal/react';

const { open, close, isReady, isLoading, error } = useCheckout(adapter);

useCheckout takes an optional second argument { timeout?, scriptUrl? } forwarded to the loader. The hook cleans up on unmount and is safe in React 18 strict mode.

Vue 3

<script setup lang="ts">
import { useCheckout } from 'payment-universal/vue';
import { razorpayBrowser } from 'payment-universal/razorpay/browser';

const adapter = razorpayBrowser({ keyId: import.meta.env.VITE_RAZORPAY_KEY });
const { open, close, isReady, isLoading, error } = useCheckout(adapter);

async function pay() {
  const order = await fetch('/api/checkout', { method: 'POST' }).then((r) => r.json());
  const result = await open({ order, mode: 'modal' });
  // ...
}
</script>

<template>
  <button :disabled="!isReady || isLoading" @click="pay">Pay</button>
</template>

Returns reactive Ref<boolean> / Ref<Error | null> values.

Angular

Bind a gateway adapter at module level via CheckoutModule.forRoot(adapter):

// app.module.ts
import { NgModule } from '@angular/core';
import { CheckoutModule } from 'payment-universal/angular';
import { razorpayBrowser } from 'payment-universal/razorpay/browser';

@NgModule({
  imports: [
    CheckoutModule.forRoot(
      razorpayBrowser({ keyId: environment.razorpayKey }),
    ),
  ],
})
export class AppModule {}

Then inject CheckoutService anywhere:

import { CheckoutService } from 'payment-universal/angular';

constructor(private readonly checkout: CheckoutService) {}

async pay() {
  await this.checkout.load();
  const order = await firstValueFrom(this.http.post('/api/checkout', {}));
  const result = await this.checkout.open({ order, mode: 'modal' });
}

Uses PLATFORM_ID + isPlatformBrowser internally — SSR safe on Angular Universal.

Vanilla

import { PaymentClient } from 'payment-universal/vanilla';
import { razorpayBrowser } from 'payment-universal/razorpay/browser';

const client = new PaymentClient(razorpayBrowser({ keyId: 'rzp_...' }));

document.querySelector('#pay')!.addEventListener('click', async () => {
  await client.load();
  const order = await fetch('/api/checkout', { method: 'POST' }).then((r) => r.json());
  const result = await client.open({ order, mode: 'modal' });
  await fetch('/api/verify', { method: 'POST', body: JSON.stringify(result) });
});

UMD build is also available at dist/index.umd.js (global: PaymentUniversal) for script-tag usage.

Gateway configuration

Each gateway has one browser factory and one server factory. Mix gateways between environments if you're running an experiment.

Razorpay

import { razorpayBrowser } from 'payment-universal/razorpay/browser';
import { razorpayServer } from 'payment-universal/razorpay/server';

razorpayBrowser({ keyId: 'rzp_...' /* scriptUrl? */ });
razorpayServer({ keyId: 'rzp_...', keySecret: '...' /* apiBase? */ });

Supports: modal, redirect. Signature verification: HMAC-SHA256 of order_id|payment_id using keySecret, compared in constant time.

Cashfree

import { cashfreeBrowser } from 'payment-universal/cashfree/browser';
import { cashfreeServer } from 'payment-universal/cashfree/server';

cashfreeBrowser({ appId: 'CF_...', mode: 'sandbox' | 'production' });
cashfreeServer({
  appId: 'CF_...',
  secretKey: '...',
  // optional: apiBase, apiVersion
});

Supports: modal, redirect. Uses Cashfree PG v3 SDK browser-side (@cashfreepayments/cashfree-js). Server uses the Orders API (POST /pg/orders) with three-header auth. Verification fetches /pg/orders/{id}/payments and maps the latest payment_status.

PayU (redirect-only)

import { payuBrowser } from 'payment-universal/payu/browser';
import { payuServer } from 'payment-universal/payu/server';

payuBrowser(); // no config — server generates all form fields + hash
payuServer({ merchantKey: '...', merchantSalt: '...', mode: 'test' | 'production' });

Supports: redirect only. Calling open({ mode: 'modal' }) throws UnsupportedModeError synchronously. Hash: SHA-512 of the documented pipe-separated payload, constant-time verified on the response callback.

Juspay (redirect-only)

import { juspayBrowser } from 'payment-universal/juspay/browser';
import { juspayServer } from 'payment-universal/juspay/server';

juspayBrowser();
juspayServer({ apiKey: '...', merchantId: '...', mode: 'sandbox' | 'production' });

Supports: redirect only. Server creates a HyperCheckout order (POST /orders with Basic auth + x-merchantid); browser navigates to payment_links.web. Verification fetches GET /orders/{id} and maps status: 'CHARGED'paid.

Stripe (redirect-only in v1)

import { stripeBrowser } from 'payment-universal/stripe/browser';
import { stripeServer } from 'payment-universal/stripe/server';

stripeBrowser({ publishableKey: 'pk_test_...' });
stripeServer({
  secretKey: 'sk_test_...',
  successUrl: 'https://yourapp.com/success?session_id={CHECKOUT_SESSION_ID}',
  cancelUrl: 'https://yourapp.com/cancel',
});

v1 uses Stripe Checkout (hosted). Stripe Elements (embedded card form) is out of scope. The browser adapter dynamically imports @stripe/stripe-js — only required if you're actually using Stripe. You can inject your own loader via the loadStripe config option (useful in tests).

Checkout modes

await adapter.openCheckout({ order, mode: 'modal' });
// Drop-in overlay — Razorpay & Cashfree.

await adapter.openCheckout({
  order,
  mode: 'redirect',
  returnUrl: 'https://yourapp.com/order/return',
  cancelUrl: 'https://yourapp.com/order/cancel',
});
// Hosted page — all 5 gateways. The Promise never resolves:
// the user navigates away, and your returnUrl handler takes over.

Each BrowserAdapter exposes a capabilities object you can inspect without try/catch:

if (adapter.capabilities.modal) {
  await open({ order, mode: 'modal' });
} else {
  await open({ order, mode: 'redirect', returnUrl });
}

Error handling

All errors extend PaymentError. Import from the root:

import {
  PaymentError,
  CheckoutLoadError,       // gateway SDK failed to load / timed out
  CheckoutDismissedError,  // user closed the modal
  UnsupportedModeError,    // mode isn't supported by this gateway
  VerificationError,       // signature/hash mismatch or missing fields
  GatewayApiError,         // non-2xx response from gateway API
} from 'payment-universal';

try {
  const result = await open({ order, mode: 'modal' });
} catch (err) {
  if (err instanceof CheckoutDismissedError) {
    return; // user cancelled — don't treat as an error
  }
  if (err instanceof PaymentError) {
    console.error(err.code, err.gateway, err.cause);
  }
  throw err;
}

Every error carries:

  • code — a stable string like RAZORPAY_SIGNATURE_MISMATCH, CASHFREE_CREATE_ORDER_FAILED.
  • gateway'razorpay' | 'cashfree' | 'payu' | 'juspay' | 'stripe' | undefined.
  • cause — the original payload for advanced handling (never your secret).

TypeScript

All public types are exported from the root:

import type {
  // adapters
  BrowserAdapter,
  ServerAdapter,
  // requests & responses
  OrderRequest,
  NormalizedOrder,
  CheckoutOptions,
  PaymentResult,
  VerificationResult,
  // shapes
  CustomerInfo,
  Capabilities,
  CheckoutMode,
  GatewayName,
  LoadOptions,
} from 'payment-universal';

SSR safety

Designed from the first commit to run under server rendering:

  • Browser adapters guard every window / document access with typeof window !== 'undefined'.
  • Browser load() rejects on the server with CheckoutLoadError({ code: 'SSR_LOAD_BLOCKED' }).
  • useCheckout (React) only triggers load() inside useEffect.
  • useCheckout (Vue) no-ops DOM work if window is undefined.
  • CheckoutService (Angular) uses PLATFORM_ID + isPlatformBrowser.
  • Server adapters use globalThis.fetch (Node ≥ 18) + node:crypto.

The "node" conditional export on every /server entry means browser bundlers (webpack, rollup, esbuild, Vite) physically refuse to resolve server code when bundling for the browser target. Secret keys cannot leak into client bundles.

Security

  • Secrets stay on the server. Two-tier enforcement is at the bundler level, not a convention.
  • timingSafeEqual everywhere. HMAC (Razorpay) and SHA-512 (PayU) comparisons are constant-time. Lengths are pre-checked to avoid throws.
  • Cashfree / Juspay / Stripe verification is server-to-server authoritative. The adapter re-fetches payment status from the gateway's API using your secret key, so a tampered client payload cannot forge a paid status. Still, always cross-check verification.amount and verification.orderId against your own DB record before fulfilling — the library tells you the gateway's opinion, not yours.
  • All signature / field mismatches throw VerificationError, never silently return { verified: false } without a reason.
  • No secrets in error messages. error.cause holds the gateway's response body, not your keys.

Tree-shaking

Every gateway and framework lives at a separate subpath export, and sideEffects: false is set in package.json. Importing razorpayBrowser ships only Razorpay's browser adapter. No other gateway code — not even a registry entry — ends up in your bundle.

Node support

Server adapters require Node 18+ (globalThis.fetch + node:crypto). Browser adapters run wherever modern browsers + fetch run.

Out of scope (v1)

By design:

  • Subscriptions / mandates / recurring payments — mandate flows differ deeply across gateways; needs its own spec.
  • Refunds.
  • Webhook signature verification helpers.
  • Customer / vault management.
  • EMI / UPI Intent / BNPL-specific flows.
  • Stripe Elements (embedded card form) — v1 sticks to Stripe Checkout.
  • Mobile SDKs (React Native, Flutter).

Some of these are on the v2 roadmap.

Roadmap

  • [x] Core architecture + 5 gateways × {browser, server} + 4 framework adapters
  • [x] 61 unit tests, strict TypeScript, clean build
  • [x] Two-tier export separation enforced via "node" condition
  • [ ] Sandbox smoke tests per gateway (in progress)
  • [ ] More examples on the landing page
  • [ ] 1.0.0 release
  • [ ] v2: refunds, webhooks, subscriptions

Development

git clone https://github.com/Rupam-Shil/payment-universal.git
cd payment-universal
npm install
npm run typecheck   # tsc --noEmit -p tsconfig.test.json
npm test            # vitest run
npm run build       # rollup -c

Contributing

Bug reports and PRs welcome at https://github.com/Rupam-Shil/payment-universal/issues.

License

MIT © Rupam Shil