better-auth-razorpay
v2.0.5
Published
Razorpay plugin for Better Auth
Downloads
713
Maintainers
Readme
better-auth-razorpay(Community Plugin)
A comprehensive Razorpay plugin for Better Auth — subscriptions, customers, plans, webhooks, and organization billing out of the box.
Features
- 🔄 Full Subscription Lifecycle — Create, upgrade, cancel (immediate or end-of-cycle), pause, resume, restore, and query subscriptions with a single API
- 🛡️ Trial Abuse Prevention — Automatically tracks trial history per user and blocks repeat free trials across plan changes
- 👤 Automatic Customer Sync — Razorpay customers are created on sign-up and kept in sync whenever user details change
- 🏢 Organization & Seat-Based Billing — Per-organization subscriptions with automatic seat quantity sync as members join or leave
- 🔔 Secure Webhook Processing — HMAC-SHA256 verified handlers for every Razorpay subscription event with granular lifecycle callbacks
- 📋 Plan & Invoice Management — Create, list, and fetch plans; retrieve invoices; link and manage promotional offers
- ⚛️ Pre-Built React Hooks — Drop-in TanStack Query hooks with automatic cache invalidation for instant UI integration
- 🧩 Line Items & Scheduling — Usage-based add-ons via line items and deferred plan changes with
scheduleAtPeriodEnd - 🔒 End-to-End Type Safety — Full TypeScript coverage with typed error codes, status helpers, and IntelliSense-friendly APIs
Installation
npm install better-auth-razorpay razorpayQuick Start
1. Server Setup
import { betterAuth } from "better-auth";
import { razorpay } from "better-auth-razorpay";
import Razorpay from "razorpay";
const razorpayClient = new Razorpay({
key_id: process.env.RAZORPAY_KEY_ID!,
key_secret: process.env.RAZORPAY_KEY_SECRET!,
});
export const auth = betterAuth({
plugins: [
razorpay({
razorpayClient,
razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET!,
createCustomerOnSignUp: true,
subscription: {
enabled: true,
plans: [
{
planId: "plan_XXXXXXXXXX",
name: "pro",
totalCount: 12,
},
{
planId: "plan_YYYYYYYYYY",
annualPlanId: "plan_ZZZZZZZZZZ",
name: "enterprise",
totalCount: 1,
quantity: 1, // seat-based
},
],
onSubscriptionActivated: async ({ subscription, plan, event }) => {
console.log(`Subscription activated: ${subscription.id}`);
},
},
}),
],
});2. Client Setup
import { createAuthClient } from "better-auth/client";
import { razorpayClient } from "better-auth-razorpay/client";
export const client = createAuthClient({
plugins: [
razorpayClient({
subscription: true,
}),
],
});3. Webhook Endpoint
Register your webhook URL in the Razorpay Dashboard:
https://your-domain.com/api/auth/razorpay/webhookSelect the following events:
subscription.authenticatedsubscription.activatedsubscription.chargedsubscription.pendingsubscription.haltedsubscription.cancelledsubscription.completedsubscription.pausedsubscription.resumedsubscription.updated
Configuration
RazorpayOptions
| Option | Type | Required | Description |
| ------------------------- | --------------------- | -------- | --------------------------------------------------- |
| razorpayClient | Razorpay | ✅ | Razorpay SDK instance |
| razorpayWebhookSecret | string | ✅ | Webhook secret for HMAC-SHA256 verification |
| createCustomerOnSignUp | boolean | ❌ | Auto-create Razorpay customer when a user registers |
| onCustomerCreate | function | ❌ | Callback after customer creation |
| getCustomerCreateParams | function | ❌ | Customize Razorpay customer creation params |
| subscription | SubscriptionOptions | ❌ | Subscription configuration (see below) |
| organization | OrganizationOptions | ❌ | Organization billing configuration (see below) |
| onEvent | function | ❌ | Callback for all incoming webhook events |
| schema | object | ❌ | Custom schema overrides |
SubscriptionOptions
Enable subscriptions by setting subscription.enabled: true:
subscription: {
enabled: true,
plans: [...],
// Optional settings
requireEmailVerification: false,
authorizeReference: async ({ user, referenceId, action }) => true,
getSubscriptionCreateParams: async ({ user, plan }) => ({}),
// Lifecycle callbacks
onSubscriptionActivated: async ({ event, razorpaySubscription, subscription, plan }) => {},
onSubscriptionRenewed: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionAuthenticated: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionCharged: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionCancelled: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionPaused: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionResumed: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionHalted: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionPending: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionCompleted: async ({ event, razorpaySubscription, subscription }) => {},
onSubscriptionUpdated: async ({ event, razorpaySubscription, subscription }) => {},
}Plan Configuration
type RazorpayPlan = {
planId: string; // Razorpay plan ID (e.g., "plan_XXXXXXXXXX")
name: string; // Plan name used internally
annualPlanId?: string; // Optional annual plan ID for yearly billing
totalCount?: number; // Number of billing cycles
quantity?: number; // Seat-based billing quantity
group?: string; // Group name for multi-plan subscriptions
limits?: Record<string, unknown>; // Plan-specific metadata
freeTrial?: {
days: number; // Free trial duration in days
onTrialStart?: (subscription) => Promise<void>;
onTrialEnd?: ({ subscription }, ctx) => Promise<void>;
};
};API Endpoints
Subscription Endpoints
These endpoints are only available when
subscription.enabledistrue.
| Method | Path | Description |
| ------ | ----------------------- | ------------------------------------- |
| POST | /subscription/upgrade | Create or upgrade a subscription |
| POST | /subscription/cancel | Cancel an active subscription |
| POST | /subscription/pause | Pause an active subscription |
| POST | /subscription/resume | Resume a paused subscription |
| GET | /subscription/list | List subscriptions for user/reference |
| POST | /subscription/update | Update subscription (plan, quantity) |
| POST | /subscription/restore | Restore pending-cancellation sub |
| GET | /subscription/get | Get a subscription by local DB ID |
Razorpay-Specific Endpoints
| Method | Path | Description |
| ------ | --------------------------------------- | ----------------------------------- |
| GET | /razorpay/subscription/get | Fetch subscription by Razorpay ID |
| POST | /razorpay/subscription-link | Create a subscription link |
| GET | /razorpay/subscription/pending-update | Get pending update details |
| POST | /razorpay/subscription/cancel-update | Cancel a pending update |
| GET | /razorpay/subscription/invoices | Fetch subscription invoices |
| POST | /razorpay/subscription/link-offer | Link an offer to a subscription |
| POST | /razorpay/subscription/delete-offer | Unlink an offer from a subscription |
Plan Endpoints
| Method | Path | Description |
| ------ | ----------------------- | -------------------------- |
| POST | /razorpay/plan/create | Create a new Razorpay plan |
| GET | /razorpay/plan/list | List all Razorpay plans |
| GET | /razorpay/plan/get | Fetch a plan by ID |
Customer Endpoints
| Method | Path | Description |
| ------ | --------------------------- | -------------------------- |
| POST | /razorpay/customer/create | Create a Razorpay customer |
| POST | /razorpay/customer/edit | Edit customer details |
| GET | /razorpay/customer/list | List all customers |
| GET | /razorpay/customer/get | Fetch a customer by ID |
Webhook
| Method | Path | Description |
| ------ | ------------------- | ------------------------- |
| POST | /razorpay/webhook | Razorpay webhook receiver |
Usage Examples
Upgrade Subscription
// Client-side
const { data } = await client.subscription.upgrade({
plan: "pro",
});
// Annual plan
const { data } = await client.subscription.upgrade({
plan: "pro",
annual: true,
});
// With add-on line items (usage-based billing)
const { data } = await client.subscription.upgrade({
plan: "pro",
lineItems: [
{ item_id: "item_XXXXXXX", quantity: 5 },
],
});
// Schedule plan change at end of billing period (avoid mid-cycle proration)
const { data } = await client.subscription.upgrade({
plan: "enterprise",
scheduleAtPeriodEnd: true,
});Cancel Subscription
// Cancel immediately
await client.subscription.cancel({});
// Cancel at end of billing cycle
await client.subscription.cancel({
cancelAtCycleEnd: true,
});Pause / Resume
await client.subscription.pause({});
await client.subscription.resume({});List & Get Subscriptions
// List all subscriptions
const { data: subscriptions } = await client.subscription.list({});
// Get specific subscription by its local DB ID
const { data: sub } = await client.subscription.get({
query: { subscriptionId: "local_sub_id" }
});Organization Subscription
// Subscribe on behalf of an organization
const { data } = await client.subscription.upgrade({
plan: "enterprise",
referenceId: "org_123",
customerType: "organization",
});React Hooks (TanStack Query)
The plugin exports a full suite of TanStack query/mutation hooks under better-auth-razorpay/react. Simply install @tanstack/react-query and import them:
import {
useSubscription,
useSubscriptionList,
useUpgradeSubscription,
useSubscriptionStatus
} from "better-auth-razorpay/react";
function Billing() {
const { data: subscriptions, isLoading } = useSubscriptionList(authClient);
const { mutate: upgrade } = useUpgradeSubscription(authClient);
const status = useSubscriptionStatus(subscriptions?.[0]);
if (status.isTrialing) return <p>You are on a free trial!</p>;
return <button onClick={() => upgrade({ plan: "pro" })}>Upgrade</button>
}Organization Integration
Enable organization-level billing by adding the organization option. This requires the Better Auth Organization plugin.
razorpay({
razorpayClient,
razorpayWebhookSecret: "...",
organization: {
enabled: true,
getCustomerCreateParams: async (organization, ctx) => ({
name: organization.name,
email: `billing@${organization.slug}.com`,
}),
onCustomerCreate: async ({ razorpayCustomer, organization }) => {
console.log(`Org customer created: ${razorpayCustomer.id}`);
},
},
subscription: {
enabled: true,
plans: [
{
planId: "plan_TEAM",
name: "team",
quantity: 1, // seat-based
},
],
authorizeReference: async ({ user, referenceId, action }) => {
// Check if user has permission to manage org subscriptions
return true;
},
},
});What the organization integration does automatically:
- Syncs organization name to Razorpay customer when the org is updated
- Blocks deletion of organizations with active subscriptions
- Syncs seat count when members are added/removed/accept invitations
- Adds
razorpayCustomerIdfield to theorganizationtable
Database Schema
The plugin automatically extends your database with the following tables/fields:
user table (extended)
| Field | Type | Description |
| -------------------- | --------- | -------------------- |
| razorpayCustomerId | string? | Razorpay customer ID |
subscription table (new)
| Field | Type | Description |
| ------------------------ | ---------- | ------------------------------------------ |
| id | string | Primary key |
| plan | string | Plan name |
| referenceId | string | User ID or organization ID |
| razorpayCustomerId | string? | Razorpay customer ID |
| razorpaySubscriptionId | string? | Razorpay subscription ID |
| razorpayPlanId | string? | Razorpay plan ID |
| status | string | Subscription status (default: "created") |
| currentStart | date? | Current billing cycle start |
| currentEnd | date? | Current billing cycle end |
| endedAt | date? | When subscription ended |
| quantity | number? | Seat quantity (default: 1) |
| totalCount | number? | Total billing cycles |
| paidCount | number? | Completed billing cycles (default: 0) |
| remainingCount | number? | Remaining billing cycles |
| cancelledAt | date? | Cancellation timestamp |
| pausedAt | date? | Pause timestamp |
| shortUrl | string? | Payment authorization URL |
| cancelAtCycleEnd | boolean? | Scheduled cancellation flag |
| billingPeriod | string? | Billing period |
| trialStart | date? | Trial period start date |
| trialEnd | date? | Trial period end date |
| metadata | string? | Custom JSON stringified metadata |
| renewedAt | date? | Last renewal timestamp |
organization table (extended, when enabled)
| Field | Type | Description |
| -------------------- | --------- | -------------------- |
| razorpayCustomerId | string? | Razorpay customer ID |
Subscription Status Lifecycle
stateDiagram-v2
[*] --> created
created --> authenticated : Customer authorizes
authenticated --> active : First payment success
active --> paused : Pause requested
paused --> active : Resume requested
active --> pending : Payment retry in progress
pending --> active : Retry succeeds
pending --> halted : All retries exhausted
halted --> active : Manual resolution
active --> cancelled : Cancellation
active --> completed : All cycles done
cancelled --> [*]
completed --> [*]
expired --> [*]| Status | Description |
| --------------- | -------------------------------------------- |
| created | Subscription created, awaiting authorization |
| authenticated | Customer authorized, awaiting first charge |
| active | Active and charging normally |
| pending | Payment retry in progress |
| halted | All payment retries exhausted |
| paused | Temporarily paused |
| cancelled | Cancelled (immediate or end-of-cycle) |
| completed | All billing cycles completed |
| expired | Subscription expired |
Utility Functions
Import these helpers for subscription status checks:
import {
isActive,
isAuthenticated,
isPaused,
isCancelled,
isTerminal,
isUsable,
hasPaymentIssue,
toSubscriptionStatus,
timestampToDate,
} from "better-auth-razorpay";| Function | Description |
| --------------------------- | -------------------------------------------------------- |
| isActive(sub) | Status is "active" |
| isAuthenticated(sub) | Status is "authenticated" |
| isPaused(sub) | Status is "paused" |
| isCancelled(sub) | Status is "cancelled" |
| isTerminal(sub) | Status is "cancelled", "completed", or "expired" |
| isUsable(sub) | Status is "active" or "authenticated" |
| hasPaymentIssue(sub) | Status is "pending" or "halted" |
| toSubscriptionStatus(str) | Convert a string to a typed RazorpaySubscriptionStatus |
| timestampToDate(ts) | Convert Unix timestamp (seconds) to Date |
Error Codes
All error codes are exported and can be used for client-side matching:
import { RAZORPAY_ERROR_CODES } from "better-auth-razorpay/client";| Code | Message |
| -------------------------------------- | ---------------------------------------------- |
| UNAUTHORIZED | Unauthorized access |
| SUBSCRIPTION_NOT_FOUND | Subscription not found |
| SUBSCRIPTION_PLAN_NOT_FOUND | Subscription plan not found |
| ALREADY_SUBSCRIBED_PLAN | Already subscribed to this plan |
| CUSTOMER_NOT_FOUND | Razorpay customer not found |
| EMAIL_VERIFICATION_REQUIRED | Email verification required before subscribing |
| SUBSCRIPTION_NOT_ACTIVE | Subscription is not active |
| SUBSCRIPTION_ALREADY_CANCELLED | Subscription is already cancelled |
| SUBSCRIPTION_ALREADY_PAUSED | Subscription is already paused |
| SUBSCRIPTION_NOT_PAUSED | Subscription is not paused |
| ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION | Cannot delete org with active subscription |
| FAILED_TO_VERIFY_WEBHOOK | Webhook signature verification failed |
| Code | Message |
| --------------------------------------- | --------------------------------------------- |
| UNAUTHORIZED | Unauthorized access |
| INVALID_REQUEST_BODY | Invalid request body |
| SUBSCRIPTION_NOT_FOUND | Subscription not found |
| SUBSCRIPTION_PLAN_NOT_FOUND | Subscription plan not found |
| ALREADY_SUBSCRIBED_PLAN | You're already subscribed to this plan |
| REFERENCE_ID_NOT_ALLOWED | Reference id is not allowed |
| CUSTOMER_NOT_FOUND | Razorpay customer not found for this user |
| UNABLE_TO_CREATE_CUSTOMER | Unable to create Razorpay customer |
| WEBHOOK_SIGNATURE_NOT_FOUND | Razorpay webhook signature not found |
| WEBHOOK_SECRET_NOT_FOUND | Razorpay webhook secret not found |
| WEBHOOK_ERROR | Razorpay webhook error |
| FAILED_TO_VERIFY_WEBHOOK | Failed to verify Razorpay webhook signature |
| FAILED_TO_FETCH_PLANS | Failed to fetch plans |
| EMAIL_VERIFICATION_REQUIRED | Email verification required |
| SUBSCRIPTION_NOT_ACTIVE | Subscription is not active |
| SUBSCRIPTION_ALREADY_CANCELLED | Subscription is already cancelled |
| SUBSCRIPTION_ALREADY_PAUSED | Subscription is already paused |
| SUBSCRIPTION_NOT_PAUSED | Subscription is not paused |
| ORGANIZATION_NOT_FOUND | Organization not found |
| ORGANIZATION_SUBSCRIPTION_NOT_ENABLED | Organization subscription not enabled |
| AUTHORIZE_REFERENCE_REQUIRED | authorizeReference callback required |
| ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION | Cannot delete org with active subscription |
| ORGANIZATION_REFERENCE_ID_REQUIRED | Reference ID or activeOrganizationId required |
| PLAN_NOT_FOUND | Razorpay plan not found |
| PLAN_CREATE_FAILED | Failed to create Razorpay plan |
| SUBSCRIPTION_CANCEL_FAILED | Failed to cancel subscription |
| SUBSCRIPTION_PAUSE_FAILED | Failed to pause subscription |
| SUBSCRIPTION_RESUME_FAILED | Failed to resume subscription |
| SUBSCRIPTION_UPDATE_FAILED | Failed to update subscription |
| SUBSCRIPTION_LINK_CREATE_FAILED | Failed to create subscription link |
| INVOICE_FETCH_FAILED | Failed to fetch invoices |
| OFFER_LINK_FAILED | Failed to link offer |
| OFFER_DELETE_FAILED | Failed to delete offer |
| CUSTOMER_EDIT_FAILED | Failed to edit customer |
| PENDING_UPDATE_NOT_FOUND | No pending update found |
| PENDING_UPDATE_CANCEL_FAILED | Failed to cancel pending update |
Webhook Events
The plugin automatically handles these Razorpay webhook events:
| Event | Internal Handler | Description |
| ---------------------------- | ----------------------------- | --------------------------------------------- |
| subscription.authenticated | onSubscriptionAuthenticated | Customer authorized the subscription |
| subscription.activated | onSubscriptionActivated | First payment successful, subscription active |
| subscription.charged | onSubscriptionCharged | Recurring payment charged successfully |
| subscription.pending | onSubscriptionPending | Payment retry in progress |
| subscription.halted | onSubscriptionHalted | All payment retries exhausted |
| subscription.cancelled | onSubscriptionCancelled | Subscription cancelled |
| subscription.completed | onSubscriptionCompleted | All billing cycles completed |
| subscription.paused | onSubscriptionPaused | Subscription paused |
| subscription.resumed | onSubscriptionResumed | Subscription resumed |
| subscription.updated | onSubscriptionUpdated | Plan/quantity updated |
Each handler automatically:
- Verifies the webhook signature (HMAC-SHA256)
- Finds the corresponding local subscription record
- Updates the subscription status in your database
- Calls your optional lifecycle callback
Free Trials
Configure free trials per plan:
plans: [
{
planId: "plan_PRO",
name: "pro",
totalCount: 12,
freeTrial: {
days: 14,
onTrialStart: async (subscription) => {
// Grant trial access
},
onTrialEnd: async ({ subscription }, ctx) => {
// Trial ended, first payment charged
},
},
},
],The plugin sets Razorpay's start_at parameter to defer the first charge by the configured number of trial days.
Trial Abuse Prevention: The plugin automatically tracks trialStart in the local database. If a user has ever had a trial on any subscription, they are permanently blocked from receiving another free trial if they upgrade or resubscribe. They will be charged immediately.
Seat-Based Billing
For per-seat pricing with organizations:
plans: [
{
planId: "plan_TEAM",
name: "team",
quantity: 1, // enables seat-based billing
},
],When quantity is set and the organization plugin is enabled, the plugin automatically syncs the seat count with Razorpay whenever:
- A member is added to the organization
- A member is removed from the organization
- An invitation is accepted
Environment Variables
RAZORPAY_KEY_ID=rzp_test_XXXXXXXXXX
RAZORPAY_KEY_SECRET=YYYYYYYYYYYYYYYY
RAZORPAY_WEBHOOK_SECRET=ZZZZZZZZZZZZZZZZLicense
MIT
