@digibuffer/cone-pay
v0.3.1
Published
Minimal, reusable Razorpay integration core — create orders/plans/subscriptions on the server, open the checkout modal on the client, verify checkout/webhook signatures. No webhook handling or app-specific logic.
Maintainers
Readme
@digibuffer/cone-pay
Minimal, reusable Razorpay integration for Node/Next.js apps.
Scope is deliberately narrow: this package only does the parts that are identical in every integration — creating an order/plan/subscription on the server, opening the checkout modal on the client, and verifying the signature the modal hands back. It does not handle webhooks, entitlement/subscription state, or anything about what a payment unlocks in your app — that logic differs per app and belongs there, not here.
Installation
npm install @digibuffer/cone-payServer — create an order or subscription
// src/lib/razorpay.ts
import { createRazorpayClient } from "@digibuffer/cone-pay"
export const razorpay = createRazorpayClient({
keyId: process.env.RAZORPAY_KEY_ID!,
keySecret: process.env.RAZORPAY_KEY_SECRET!,
})One-time payment:
// app/api/checkout/route.ts
const order = await razorpay.createOrder({
amount: 49900, // ₹499.00, smallest currency unit
currency: "INR",
receipt: `order_${userId}_${Date.now()}`,
})
return Response.json({ orderId: order.id, amount: order.amount })Recurring payment:
const plan = await razorpay.createPlan({
period: "monthly",
interval: 1,
name: "Pro plan",
amount: 99900,
})
const subscription = await razorpay.createSubscription({
planId: plan.id,
totalCount: 12, // bill 12 times, then stop
})Client — open the checkout modal
"use client"
import { openRazorpayCheckout, RazorpayCheckoutDismissedError } from "@digibuffer/cone-pay/client"
async function handlePay() {
const { orderId, amount } = await fetch("/api/checkout", { method: "POST" }).then((r) => r.json())
try {
const payment = await openRazorpayCheckout({
key: process.env.NEXT_PUBLIC_RAZORPAY_KEY_ID!,
orderId,
amount,
name: "Your App",
prefill: { email: user.email },
})
// Send this to your own API route to verify + fulfill.
await fetch("/api/checkout/confirm", {
method: "POST",
body: JSON.stringify(payment),
})
} catch (err) {
if (err instanceof RazorpayCheckoutDismissedError) return // user closed the modal
throw err
}
}Server — verify the payment before fulfilling it
The modal's handler response is not proof of payment on its own — always verify the signature server-side before granting anything.
// app/api/checkout/confirm/route.ts
import { verifyRazorpayOrderSignature } from "@digibuffer/cone-pay"
const body = await req.json()
const ok = verifyRazorpayOrderSignature({
orderId: body.razorpay_order_id,
paymentId: body.razorpay_payment_id,
signature: body.razorpay_signature,
keySecret: process.env.RAZORPAY_KEY_SECRET!,
})
if (!ok) return new Response("Invalid signature", { status: 400 })
// Now it's your app's job: mark the order paid, grant the entitlement, etc.For a subscription payment, use verifyRazorpaySubscriptionSignature (same shape, takes subscriptionId instead of orderId) with razorpay_subscription_id from the response.
Server — verify a webhook delivery
Webhooks are the reliable source of truth for async events (a renewal charge, a delayed payment capture) — the checkout-confirm step above is just a fast UX shortcut, not something to rely on alone. Verifying the delivery is mechanical; deciding what to do with it is your app's job.
// app/api/webhooks/razorpay/route.ts
import { verifyRazorpayWebhookSignature } from "@digibuffer/cone-pay"
const body = await req.text() // raw body — signing is over exact bytes, don't parse first
const signature = req.headers.get("x-razorpay-signature") ?? ""
if (!verifyRazorpayWebhookSignature(body, signature, process.env.RAZORPAY_WEBHOOK_SECRET!)) {
return new Response("Invalid signature", { status: 400 })
}
const event = JSON.parse(body)
// Now it's your app's job: log the event, dispatch on event.event, update your own tables.The webhook secret is configured separately from your API key secret — set it when you register the webhook URL in the Razorpay dashboard.
What's intentionally not in here
- Webhook handling. Razorpay's webhooks (
payment.captured,subscription.charged, etc.) are the reliable source of truth for async/offline events — this package verifies a delivery's signature, but what you do with it — update a DB row, send an email, revoke access — is 100% app-specific. Dispatch and handle events in each app. - Entitlement / subscription state. Whether a user is "pro", when their access expires, what a plan unlocks — that's your app's data model, not this package's.
- Currency formatting, pricing pages, invoices, refunds. Out of scope for the same reason.
API reference
@digibuffer/cone-pay (server)
createRazorpayClient({ keyId, keySecret })→{ createOrder, createPlan, createSubscription, fetchSubscription, fetchPayment, cancelSubscription }fetchSubscription(subscriptionId)/fetchPayment(paymentId)— read current state straight from Razorpay's API, for persisting it right after checkout closes instead of waiting on a webhook (self-hosted/local setups can't always receive one)cancelSubscription({ subscriptionId, cancelAtCycleEnd? })— immediately by default, or at the end of the current billing cycle
verifyRazorpayOrderSignature({ orderId, paymentId, signature, keySecret })→booleanverifyRazorpaySubscriptionSignature({ subscriptionId, paymentId, signature, keySecret })→booleanverifyRazorpayWebhookSignature(body, signature, webhookSecret)→boolean
@digibuffer/cone-pay/client (browser, "use client")
loadRazorpayCheckout()→Promise<void>— injectscheckout.jsonceopenRazorpayCheckout(options)→Promise<RazorpayPaymentSuccess>— rejects withRazorpayCheckoutDismissedErrorif closed without paying
