@deiondz/better-auth-razorpay 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,7 @@ export const auth = betterAuth({
84
84
  razorpayPlugin({
85
85
  razorpayClient,
86
86
  razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
87
+ razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
87
88
  createCustomerOnSignUp: true, // optional
88
89
  subscription: {
89
90
  enabled: true,
@@ -111,7 +112,9 @@ export const auth = betterAuth({
111
112
  })
112
113
  ```
113
114
 
114
- 3. **Add Client Plugin**
115
+ 3. **Add Client Plugin (required to avoid 404s)**
116
+
117
+ Add the Razorpay client plugin so requests use the correct paths. **Without it, calls like `authClient.api.get('/razorpay/get-plans')` can hit wrong URLs (e.g. `POST /api/auth/api/get` 404).** The plugin exposes `authClient.razorpay.*` methods that use the plugin’s route map.
115
118
 
116
119
  ```typescript
117
120
  // src/lib/auth-client.ts
@@ -141,6 +144,7 @@ BETTER_AUTH_URL=https://yourdomain.com
141
144
  RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxx
142
145
  RAZORPAY_KEY_SECRET=your_secret_key
143
146
  RAZORPAY_WEBHOOK_SECRET=your_webhook_secret
147
+ # Pass RAZORPAY_KEY_SECRET as razorpayKeySecret in plugin options to enable the verify-payment endpoint (same as client key, not webhook secret).
144
148
  ```
145
149
 
146
150
  5. **Run Database Migration**
@@ -163,6 +167,7 @@ npx @better-auth/cli@latest generate
163
167
  interface RazorpayPluginOptions {
164
168
  razorpayClient: Razorpay // Required: Initialized Razorpay instance (key_id, key_secret)
165
169
  razorpayWebhookSecret?: string // Optional: Webhook secret for signature verification
170
+ razorpayKeySecret?: string // Optional: API key secret for payment signature verification; when set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret)
166
171
  createCustomerOnSignUp?: boolean // Optional: Create Razorpay customer on user sign-up (default: false)
167
172
  onCustomerCreate?: (args) => Promise<void>
168
173
  getCustomerCreateParams?: (args) => Promise<{ params?: Record<string, unknown> }>
@@ -197,6 +202,7 @@ Example: using callbacks in your config:
197
202
  razorpayPlugin({
198
203
  razorpayClient,
199
204
  razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
205
+ razorpayKeySecret: process.env.RAZORPAY_KEY_SECRET, // optional: enables verify-payment endpoint
200
206
  createCustomerOnSignUp: true,
201
207
  onCustomerCreate: async ({ user, razorpayCustomer }) => {
202
208
  console.log(`Razorpay customer created for user ${user.id}: ${razorpayCustomer.id}`)
@@ -429,7 +435,7 @@ if (response.success && response.data) {
429
435
 
430
436
  ### 4. Verify Payment
431
437
 
432
- Verify payment signature after Razorpay checkout completion.
438
+ Verify payment signature after Razorpay checkout completion. This endpoint is **only registered when `razorpayKeySecret`** is set in plugin options. Use the same API key secret as your Razorpay client (not the webhook secret).
433
439
 
434
440
  **Endpoint:** `POST /api/auth/razorpay/verify-payment`
435
441
 
@@ -484,6 +490,7 @@ const handlePaymentSuccess = async (razorpayResponse: {
484
490
  - `SIGNATURE_VERIFICATION_FAILED` - Invalid payment signature
485
491
  - `UNAUTHORIZED` - User not authenticated
486
492
  - `SUBSCRIPTION_NOT_FOUND` - Subscription record not found
493
+ - `FORBIDDEN` - Subscription does not belong to authenticated user
487
494
 
488
495
  ---
489
496
 
@@ -663,23 +670,42 @@ Handle Razorpay webhook events (automatically called by Razorpay).
663
670
 
664
671
  ## Client Usage
665
672
 
666
- ### Better Auth Client Methods
673
+ ### Preferred: authClient.razorpay.* (method-based API)
667
674
 
668
- The plugin integrates with Better Auth's client API. Use `authClient.api` for all endpoint calls:
675
+ When you add `razorpayClientPlugin()` to `createAuthClient({ plugins: [...] })`, the client gets `authClient.razorpay` with explicit methods. **Use these so requests hit the correct paths** (avoids 404s like `POST /api/auth/api/get`):
669
676
 
670
677
  ```typescript
671
678
  import { authClient } from '@/lib/auth-client'
672
679
 
673
- // GET request
674
- const plans = await authClient.api.get('/razorpay/get-plans')
680
+ // GET plans
681
+ const plansRes = await authClient.razorpay.getPlans()
682
+ if (plansRes.success) console.log(plansRes.data)
683
+
684
+ // List subscriptions
685
+ const listRes = await authClient.razorpay.listSubscriptions({ referenceId: 'optional' })
675
686
 
676
- // POST request (create or update subscription)
677
- const result = await authClient.api.post('/razorpay/subscription/create-or-update', {
678
- body: { plan: 'Starter', annual: false, seats: 1 },
687
+ // Create or update subscription (returns checkoutUrl for Razorpay payment page)
688
+ const result = await authClient.razorpay.createOrUpdateSubscription({
689
+ plan: 'Starter',
690
+ annual: false,
691
+ seats: 1,
692
+ })
693
+ if (result.success) window.location.href = result.data.checkoutUrl
694
+
695
+ // Cancel, restore, verify payment
696
+ await authClient.razorpay.cancelSubscription({ subscriptionId: 'sub_xxx', immediately: false })
697
+ await authClient.razorpay.restoreSubscription({ subscriptionId: 'sub_xxx' })
698
+ await authClient.razorpay.verifyPayment({
699
+ razorpay_payment_id: 'pay_xxx',
700
+ razorpay_subscription_id: 'sub_xxx',
701
+ razorpay_signature: '...',
679
702
  })
680
- // result.data.checkoutUrl -> redirect user to Razorpay payment page
681
703
  ```
682
704
 
705
+ ### Fallback: authClient.api (only if client plugin is not used)
706
+
707
+ If you do not add the client plugin, you can use `authClient.api.get/post` with the Razorpay paths. **This can lead to 404s** (e.g. `POST /api/auth/api/get`) depending on how your auth client resolves paths. Prefer adding the client plugin and using `authClient.razorpay.*` instead.
708
+
683
709
  ### Type Safety
684
710
 
685
711
  Infer types from your auth configuration:
@@ -699,7 +725,7 @@ type User = typeof authClient.$Infer.Session.user
699
725
 
700
726
  ## TanStack Query Hooks
701
727
 
702
- The plugin works with **TanStack Query**: you can use it with `useQuery`/`useMutation` and `authClient.api` in any way you like. We provide optional pre-built hooks below; if you prefer a different setup, build your own hooks around the same endpoints (e.g. `authClient.api.get('/razorpay/get-plans')`, `authClient.api.post('/razorpay/subscription/create-or-update', { body })`, etc.).
728
+ The plugin works with **TanStack Query**. We provide optional pre-built hooks that accept your auth client; when you use `razorpayClientPlugin()`, the hooks call `authClient.razorpay.*` so requests hit the correct paths. If you prefer a different setup, use `authClient.razorpay.getPlans()`, `authClient.razorpay.createOrUpdateSubscription(...)`, etc., or build your own hooks around those methods.
703
729
 
704
730
  To use our pre-built hooks, install peer dependencies:
705
731
 
@@ -1352,38 +1378,43 @@ async function initializeRazorpayCheckout(subscriptionId: string) {
1352
1378
 
1353
1379
  ### Common Issues
1354
1380
 
1355
- **1. "Plan not found in configured plans"**
1381
+ **1. "POST /api/auth/api/get 404" or Razorpay requests returning 404**
1382
+ - Add the **client plugin** to your auth client: `createAuthClient({ plugins: [razorpayClientPlugin(), ...] })`
1383
+ - Use **method-based API** instead of `authClient.api.get/post`: call `authClient.razorpay.getPlans()`, `authClient.razorpay.createOrUpdateSubscription(...)`, etc., so requests use the plugin’s route map and hit the correct paths
1384
+ - The TanStack hooks (`usePlans`, `useSubscriptions`, etc.) use `authClient.razorpay` when present, so they work correctly once the client plugin is added
1385
+
1386
+ **2. "Plan not found in configured plans"**
1356
1387
  - Ensure the plan ID exists in Razorpay dashboard
1357
1388
  - Add the plan ID to the `plans` array in plugin configuration
1358
1389
  - Re-run Better Auth CLI after updating plans
1359
1390
 
1360
- **2. "Webhook signature verification failed"**
1391
+ **3. "Webhook signature verification failed"**
1361
1392
  - Verify webhook secret matches Razorpay dashboard
1362
1393
  - Ensure webhook URL is correct: `https://yourdomain.com/api/auth/razorpay/webhook`
1363
1394
  - Check that request body is not modified
1364
1395
  - Verify `x-razorpay-signature` header is present
1365
1396
 
1366
- **3. "Subscription already exists"**
1397
+ **4. "Subscription already exists"**
1367
1398
  - User already has an active subscription
1368
1399
  - Cancel or pause existing subscription first
1369
1400
  - Check subscription status before creating new one
1370
1401
 
1371
- **4. "User not authenticated"**
1402
+ **5. "User not authenticated"**
1372
1403
  - Ensure user is logged in via Better Auth
1373
1404
  - Check session middleware is properly configured
1374
1405
  - Verify `sessionMiddleware` is used in endpoint configuration
1375
1406
 
1376
- **5. "Subscription not found"**
1407
+ **6. "Subscription not found"**
1377
1408
  - Subscription may have been deleted
1378
1409
  - Check subscription ID is correct
1379
1410
  - Verify subscription belongs to the user
1380
1411
 
1381
- **6. Database Schema Issues**
1412
+ **7. Database Schema Issues**
1382
1413
  - Run `npx @better-auth/cli@latest migrate` after adding plugin
1383
1414
  - For Prisma/Drizzle: Run `npx @better-auth/cli@latest generate`
1384
1415
  - Check that user additional fields are properly configured
1385
1416
 
1386
- **7. Type Errors**
1417
+ **8. Type Errors**
1387
1418
  - Ensure you're using `createAuthClient<typeof auth>()` for type inference
1388
1419
  - Import types from `@deiondz/better-auth-razorpay`
1389
1420
  - Check that plugin is properly exported
package/api/index.ts CHANGED
@@ -3,4 +3,5 @@ export { createOrUpdateSubscription } from './create-or-update-subscription'
3
3
  export { getPlans } from './get-plans'
4
4
  export { listSubscriptions } from './list-subscriptions'
5
5
  export { restoreSubscription } from './restore-subscription'
6
+ export { verifyPayment } from './verify-payment'
6
7
  export { webhook } from './webhook'
@@ -0,0 +1,87 @@
1
+ import { createHmac } from 'node:crypto'
2
+ import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
3
+ import {
4
+ handleRazorpayError,
5
+ verifyPaymentSchema,
6
+ type SubscriptionRecord,
7
+ } from '../lib'
8
+
9
+ /**
10
+ * POST /api/auth/razorpay/verify-payment
11
+ * Verifies payment signature after Razorpay subscription checkout completion.
12
+ * Requires razorpayKeySecret to be set in plugin options.
13
+ */
14
+ export const verifyPayment = (keySecret: string) =>
15
+ createAuthEndpoint(
16
+ '/razorpay/verify-payment',
17
+ { method: 'POST', use: [sessionMiddleware] },
18
+ async (ctx) => {
19
+ try {
20
+ const body = verifyPaymentSchema.parse(ctx.body)
21
+ const { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } = body
22
+
23
+ const generatedSignature = createHmac('sha256', keySecret)
24
+ .update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
25
+ .digest('hex')
26
+
27
+ if (generatedSignature !== razorpay_signature) {
28
+ return {
29
+ success: false,
30
+ error: {
31
+ code: 'SIGNATURE_VERIFICATION_FAILED',
32
+ description: 'Payment signature verification failed',
33
+ },
34
+ }
35
+ }
36
+
37
+ const userId = ctx.context.session?.user?.id
38
+ if (!userId) {
39
+ return {
40
+ success: false,
41
+ error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
42
+ }
43
+ }
44
+
45
+ const record = (await ctx.context.adapter.findOne({
46
+ model: 'subscription',
47
+ where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
48
+ })) as SubscriptionRecord | null
49
+
50
+ if (!record) {
51
+ return {
52
+ success: false,
53
+ error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
54
+ }
55
+ }
56
+
57
+ if (record.referenceId !== userId) {
58
+ return {
59
+ success: false,
60
+ error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
61
+ }
62
+ }
63
+
64
+ await ctx.context.adapter.update({
65
+ model: 'subscription',
66
+ where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
67
+ update: {
68
+ data: {
69
+ status: 'pending',
70
+ updatedAt: new Date(),
71
+ },
72
+ },
73
+ })
74
+
75
+ return {
76
+ success: true,
77
+ data: {
78
+ message: 'Payment verified successfully',
79
+ payment_id: razorpay_payment_id,
80
+ subscription_id: razorpay_subscription_id,
81
+ },
82
+ }
83
+ } catch (error) {
84
+ return handleRazorpayError(error)
85
+ }
86
+ }
87
+ )
package/client/hooks.ts CHANGED
@@ -17,11 +17,13 @@ import type {
17
17
  CancelSubscriptionInput,
18
18
  RestoreSubscriptionInput,
19
19
  ListSubscriptionsInput,
20
+ VerifyPaymentInput,
20
21
  GetPlansResponse,
21
22
  ListSubscriptionsResponse,
22
23
  CreateOrUpdateSubscriptionResponse,
23
24
  CancelSubscriptionResponse,
24
25
  RestoreSubscriptionResponse,
26
+ VerifyPaymentResponse,
25
27
  RazorpayApiError,
26
28
  } from './types'
27
29
 
@@ -44,65 +46,90 @@ function assertSuccess<T>(res: unknown): asserts res is { success: true; data: T
44
46
  throw new Error('Invalid response')
45
47
  }
46
48
 
47
- /** Fetch plans (GET /razorpay/get-plans). */
49
+ /** Fetch plans (GET /razorpay/get-plans). Prefers client.razorpay when available to avoid 404s. */
48
50
  async function fetchPlans(client: RazorpayAuthClient): Promise<PlanSummary[]> {
49
- const res = await client.api.get(`${BASE}/get-plans`)
51
+ const res = client.razorpay
52
+ ? await client.razorpay.getPlans()
53
+ : await client.api.get(`${BASE}/get-plans`)
50
54
  assertSuccess<PlanSummary[]>(res)
51
55
  return res.data
52
56
  }
53
57
 
54
- /** Fetch subscriptions list (GET /razorpay/subscription/list). */
58
+ /** Fetch subscriptions list (GET /razorpay/subscription/list). Prefers client.razorpay when available. */
55
59
  async function fetchSubscriptions(
56
60
  client: RazorpayAuthClient,
57
61
  input?: ListSubscriptionsInput
58
62
  ): Promise<ListSubscriptionsResponse['data']> {
59
- const query: Record<string, string> = {}
60
- if (input?.referenceId) query.referenceId = input.referenceId
61
- const path = `${BASE}/subscription/list`
62
- const res =
63
- Object.keys(query).length > 0
64
- ? await client.api.get(path, { query })
65
- : await client.api.get(path)
63
+ const res = client.razorpay
64
+ ? await client.razorpay.listSubscriptions(input)
65
+ : (() => {
66
+ const query: Record<string, string> = {}
67
+ if (input?.referenceId) query.referenceId = input.referenceId
68
+ const path = `${BASE}/subscription/list`
69
+ return Object.keys(query).length > 0
70
+ ? client.api.get(path, { query })
71
+ : client.api.get(path)
72
+ })()
66
73
  assertSuccess<ListSubscriptionsResponse['data']>(res)
67
74
  return res.data
68
75
  }
69
76
 
70
- /** Create or update subscription (POST /razorpay/subscription/create-or-update). */
77
+ /** Create or update subscription (POST /razorpay/subscription/create-or-update). Prefers client.razorpay when available. */
71
78
  async function createOrUpdateSubscription(
72
79
  client: RazorpayAuthClient,
73
80
  input: CreateOrUpdateSubscriptionInput
74
81
  ): Promise<CreateOrUpdateSubscriptionResponse['data']> {
75
- const res = await client.api.post(`${BASE}/subscription/create-or-update`, {
76
- body: input as unknown as Record<string, unknown>,
77
- })
82
+ const res = client.razorpay
83
+ ? await client.razorpay.createOrUpdateSubscription(input)
84
+ : await client.api.post(`${BASE}/subscription/create-or-update`, {
85
+ body: input as unknown as Record<string, unknown>,
86
+ })
78
87
  assertSuccess<CreateOrUpdateSubscriptionResponse['data']>(res)
79
88
  return res.data
80
89
  }
81
90
 
82
- /** Cancel subscription (POST /razorpay/subscription/cancel). */
91
+ /** Cancel subscription (POST /razorpay/subscription/cancel). Prefers client.razorpay when available. */
83
92
  async function cancelSubscription(
84
93
  client: RazorpayAuthClient,
85
94
  input: CancelSubscriptionInput
86
95
  ): Promise<CancelSubscriptionResponse['data']> {
87
- const res = await client.api.post(`${BASE}/subscription/cancel`, {
88
- body: input as unknown as Record<string, unknown>,
89
- })
96
+ const res = client.razorpay
97
+ ? await client.razorpay.cancelSubscription(input)
98
+ : await client.api.post(`${BASE}/subscription/cancel`, {
99
+ body: input as unknown as Record<string, unknown>,
100
+ })
90
101
  assertSuccess<CancelSubscriptionResponse['data']>(res)
91
102
  return res.data
92
103
  }
93
104
 
94
- /** Restore subscription (POST /razorpay/subscription/restore). */
105
+ /** Restore subscription (POST /razorpay/subscription/restore). Prefers client.razorpay when available. */
95
106
  async function restoreSubscription(
96
107
  client: RazorpayAuthClient,
97
108
  input: RestoreSubscriptionInput
98
109
  ): Promise<RestoreSubscriptionResponse['data']> {
99
- const res = await client.api.post(`${BASE}/subscription/restore`, {
100
- body: input as unknown as Record<string, unknown>,
101
- })
110
+ const res = client.razorpay
111
+ ? await client.razorpay.restoreSubscription(input)
112
+ : await client.api.post(`${BASE}/subscription/restore`, {
113
+ body: input as unknown as Record<string, unknown>,
114
+ })
102
115
  assertSuccess<RestoreSubscriptionResponse['data']>(res)
103
116
  return res.data
104
117
  }
105
118
 
119
+ /** Verify payment (POST /razorpay/verify-payment). Prefers client.razorpay when available. */
120
+ async function verifyPayment(
121
+ client: RazorpayAuthClient,
122
+ input: VerifyPaymentInput
123
+ ): Promise<VerifyPaymentResponse['data']> {
124
+ const res = client.razorpay
125
+ ? await client.razorpay.verifyPayment(input)
126
+ : await client.api.post(`${BASE}/verify-payment`, {
127
+ body: input as unknown as Record<string, unknown>,
128
+ })
129
+ assertSuccess<VerifyPaymentResponse['data']>(res)
130
+ return res.data
131
+ }
132
+
106
133
  export type UsePlansOptions = Omit<
107
134
  UseQueryOptions<PlanSummary[], Error, PlanSummary[], readonly string[]>,
108
135
  'queryKey' | 'queryFn'
@@ -214,16 +241,19 @@ export type UseRestoreSubscriptionOptions = UseMutationOptions<
214
241
  // Re-export client types for convenience when importing from this entry
215
242
  export type {
216
243
  RazorpayAuthClient,
244
+ RazorpayClientActions,
217
245
  PlanSummary,
218
246
  CreateOrUpdateSubscriptionInput,
219
247
  CancelSubscriptionInput,
220
248
  RestoreSubscriptionInput,
221
249
  ListSubscriptionsInput,
250
+ VerifyPaymentInput,
222
251
  GetPlansResponse,
223
252
  ListSubscriptionsResponse,
224
253
  CreateOrUpdateSubscriptionResponse,
225
254
  CancelSubscriptionResponse,
226
255
  RestoreSubscriptionResponse,
256
+ VerifyPaymentResponse,
227
257
  RazorpayApiError,
228
258
  RazorpayApiResult,
229
259
  } from './types'
@@ -246,3 +276,30 @@ export function useRestoreSubscription(
246
276
  },
247
277
  })
248
278
  }
279
+
280
+ export type UseVerifyPaymentOptions = UseMutationOptions<
281
+ VerifyPaymentResponse['data'],
282
+ Error,
283
+ VerifyPaymentInput,
284
+ unknown
285
+ >
286
+
287
+ /**
288
+ * Verify payment signature after Razorpay checkout success.
289
+ * Call with the payload from the Razorpay success handler (razorpay_payment_id, razorpay_subscription_id, razorpay_signature).
290
+ * Invalidates subscriptions list on success.
291
+ */
292
+ export function useVerifyPayment(
293
+ client: RazorpayAuthClient | null | undefined,
294
+ options?: UseVerifyPaymentOptions
295
+ ) {
296
+ const queryClient = useQueryClient()
297
+ return useMutation({
298
+ mutationFn: (input: VerifyPaymentInput) => verifyPayment(client!, input),
299
+ ...options,
300
+ onSuccess: (data, variables, onMutateResult, context) => {
301
+ queryClient.invalidateQueries({ queryKey: razorpayQueryKeys.subscriptions() })
302
+ options?.onSuccess?.(data, variables, onMutateResult, context)
303
+ },
304
+ })
305
+ }
package/client/types.ts CHANGED
@@ -64,9 +64,34 @@ export type RazorpayApiResult<T = unknown> =
64
64
  | { success: true; data: T }
65
65
  | RazorpayApiError
66
66
 
67
+ /** Razorpay API actions from the client plugin (authClient.razorpay). Use these so requests hit the correct paths. */
68
+ export interface RazorpayClientActions {
69
+ getPlans: (fetchOptions?: { query?: Record<string, string> }) => Promise<RazorpayApiResult<PlanSummary[]>>
70
+ listSubscriptions: (
71
+ input?: ListSubscriptionsInput,
72
+ fetchOptions?: { query?: Record<string, string> }
73
+ ) => Promise<RazorpayApiResult<ListSubscriptionsResponse['data']>>
74
+ createOrUpdateSubscription: (
75
+ input: CreateOrUpdateSubscriptionInput,
76
+ fetchOptions?: { body?: Record<string, unknown> }
77
+ ) => Promise<RazorpayApiResult<CreateOrUpdateSubscriptionResponse['data']>>
78
+ cancelSubscription: (
79
+ input: CancelSubscriptionInput,
80
+ fetchOptions?: { body?: Record<string, unknown> }
81
+ ) => Promise<RazorpayApiResult<CancelSubscriptionResponse['data']>>
82
+ restoreSubscription: (
83
+ input: RestoreSubscriptionInput,
84
+ fetchOptions?: { body?: Record<string, unknown> }
85
+ ) => Promise<RazorpayApiResult<RestoreSubscriptionResponse['data']>>
86
+ verifyPayment: (
87
+ input: VerifyPaymentInput,
88
+ fetchOptions?: { body?: Record<string, unknown> }
89
+ ) => Promise<RazorpayApiResult<VerifyPaymentResponse['data']>>
90
+ }
91
+
67
92
  /**
68
93
  * Minimal auth client interface for Razorpay hooks.
69
- * Compatible with Better Auth's createAuthClient() return type.
94
+ * When using the client plugin (razorpayClientPlugin()), authClient.razorpay is set and hooks use it so requests hit the correct paths (avoids 404s from api.get/post).
70
95
  */
71
96
  export interface RazorpayAuthClient {
72
97
  api: {
@@ -79,6 +104,8 @@ export interface RazorpayAuthClient {
79
104
  options?: { body?: Record<string, unknown> }
80
105
  ) => Promise<RazorpayApiResult<unknown>>
81
106
  }
107
+ /** Set when razorpayClientPlugin() is used in createAuthClient({ plugins: [razorpayClientPlugin()] }). Prefer these methods over api.get/post. */
108
+ razorpay?: RazorpayClientActions
82
109
  }
83
110
 
84
111
  /** Input for create-or-update subscription. */
@@ -106,3 +133,20 @@ export interface RestoreSubscriptionInput {
106
133
  export interface ListSubscriptionsInput {
107
134
  referenceId?: string
108
135
  }
136
+
137
+ /** Input for verify-payment (Razorpay checkout success callback payload). */
138
+ export interface VerifyPaymentInput {
139
+ razorpay_payment_id: string
140
+ razorpay_subscription_id: string
141
+ razorpay_signature: string
142
+ }
143
+
144
+ /** Response shape for verify-payment (success). */
145
+ export interface VerifyPaymentResponse {
146
+ success: true
147
+ data: {
148
+ message: string
149
+ payment_id: string
150
+ subscription_id: string
151
+ }
152
+ }
package/client.ts CHANGED
@@ -1,8 +1,105 @@
1
1
  import type { BetterAuthClientPlugin } from 'better-auth/client'
2
2
  import type { razorpayPlugin } from './index'
3
+ import type {
4
+ PlanSummary,
5
+ CreateOrUpdateSubscriptionInput,
6
+ CancelSubscriptionInput,
7
+ RestoreSubscriptionInput,
8
+ ListSubscriptionsInput,
9
+ VerifyPaymentInput,
10
+ ListSubscriptionsResponse,
11
+ CreateOrUpdateSubscriptionResponse,
12
+ CancelSubscriptionResponse,
13
+ RestoreSubscriptionResponse,
14
+ VerifyPaymentResponse,
15
+ RazorpayApiResult,
16
+ } from './client/types'
3
17
 
18
+ type FetchFn = (
19
+ path: string,
20
+ options?: {
21
+ method?: string
22
+ body?: Record<string, unknown>
23
+ query?: Record<string, string>
24
+ }
25
+ ) => Promise<RazorpayApiResult<unknown>>
26
+
27
+ const PATHS = {
28
+ getPlans: '/razorpay/get-plans',
29
+ listSubscriptions: '/razorpay/subscription/list',
30
+ createOrUpdateSubscription: '/razorpay/subscription/create-or-update',
31
+ cancelSubscription: '/razorpay/subscription/cancel',
32
+ restoreSubscription: '/razorpay/subscription/restore',
33
+ verifyPayment: '/razorpay/verify-payment',
34
+ } as const
35
+
36
+ /**
37
+ * Razorpay client plugin for Better Auth.
38
+ * Exposes authClient.razorpay.* so requests use the correct paths and avoid 404s from api.get/post.
39
+ * Add to createAuthClient: plugins: [razorpayClientPlugin()]
40
+ */
4
41
  export const razorpayClientPlugin = () =>
5
42
  ({
6
43
  id: 'razorpay-plugin',
7
44
  $InferServerPlugin: {} as ReturnType<typeof razorpayPlugin>,
45
+ getActions: ($fetch: FetchFn) => ({
46
+ razorpay: {
47
+ getPlans: (fetchOptions?: Parameters<FetchFn>[1]) =>
48
+ $fetch(PATHS.getPlans, { method: 'GET', ...fetchOptions }) as Promise<
49
+ RazorpayApiResult<PlanSummary[]>
50
+ >,
51
+
52
+ listSubscriptions: (
53
+ input?: ListSubscriptionsInput,
54
+ fetchOptions?: Parameters<FetchFn>[1]
55
+ ) =>
56
+ $fetch(PATHS.listSubscriptions, {
57
+ method: 'GET',
58
+ query: input?.referenceId ? { referenceId: input.referenceId } : undefined,
59
+ ...fetchOptions,
60
+ }) as Promise<RazorpayApiResult<ListSubscriptionsResponse['data']>>,
61
+
62
+ createOrUpdateSubscription: (
63
+ input: CreateOrUpdateSubscriptionInput,
64
+ fetchOptions?: Parameters<FetchFn>[1]
65
+ ) =>
66
+ $fetch(PATHS.createOrUpdateSubscription, {
67
+ method: 'POST',
68
+ body: input as unknown as Record<string, unknown>,
69
+ ...fetchOptions,
70
+ }) as Promise<
71
+ RazorpayApiResult<CreateOrUpdateSubscriptionResponse['data']>
72
+ >,
73
+
74
+ cancelSubscription: (
75
+ input: CancelSubscriptionInput,
76
+ fetchOptions?: Parameters<FetchFn>[1]
77
+ ) =>
78
+ $fetch(PATHS.cancelSubscription, {
79
+ method: 'POST',
80
+ body: input as unknown as Record<string, unknown>,
81
+ ...fetchOptions,
82
+ }) as Promise<RazorpayApiResult<CancelSubscriptionResponse['data']>>,
83
+
84
+ restoreSubscription: (
85
+ input: RestoreSubscriptionInput,
86
+ fetchOptions?: Parameters<FetchFn>[1]
87
+ ) =>
88
+ $fetch(PATHS.restoreSubscription, {
89
+ method: 'POST',
90
+ body: input as unknown as Record<string, unknown>,
91
+ ...fetchOptions,
92
+ }) as Promise<RazorpayApiResult<RestoreSubscriptionResponse['data']>>,
93
+
94
+ verifyPayment: (
95
+ input: VerifyPaymentInput,
96
+ fetchOptions?: Parameters<FetchFn>[1]
97
+ ) =>
98
+ $fetch(PATHS.verifyPayment, {
99
+ method: 'POST',
100
+ body: input as unknown as Record<string, unknown>,
101
+ ...fetchOptions,
102
+ }) as Promise<RazorpayApiResult<VerifyPaymentResponse['data']>>,
103
+ },
104
+ }),
8
105
  }) satisfies BetterAuthClientPlugin
package/index.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  getPlans,
6
6
  listSubscriptions,
7
7
  restoreSubscription,
8
+ verifyPayment,
8
9
  webhook,
9
10
  } from './api'
10
11
  import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
@@ -21,6 +22,7 @@ import type { RazorpayPluginOptions, RazorpayUserRecord } from './lib'
21
22
  * @param options - Plugin configuration
22
23
  * @param options.razorpayClient - Initialized Razorpay instance (key_id, key_secret)
23
24
  * @param options.razorpayWebhookSecret - Webhook secret for signature verification
25
+ * @param options.razorpayKeySecret - API key secret for payment signature verification (optional; when set, enables POST /razorpay/verify-payment)
24
26
  * @param options.createCustomerOnSignUp - Create Razorpay customer when user signs up (default: false)
25
27
  * @param options.onCustomerCreate - Callback after customer is created
26
28
  * @param options.getCustomerCreateParams - Custom params when creating customer
@@ -31,6 +33,7 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
31
33
  const {
32
34
  razorpayClient,
33
35
  razorpayWebhookSecret,
36
+ razorpayKeySecret,
34
37
  createCustomerOnSignUp = false,
35
38
  onCustomerCreate,
36
39
  getCustomerCreateParams,
@@ -83,6 +86,9 @@ export const razorpayPlugin = (options: RazorpayPluginOptions) => {
83
86
  'subscription/restore': restoreSubscription(razorpay),
84
87
  'subscription/list': listSubscriptions({ subscription: subOpts }),
85
88
  'get-plans': getPlans({ subscription: subOpts }),
89
+ ...(razorpayKeySecret
90
+ ? { 'verify-payment': verifyPayment(razorpayKeySecret) }
91
+ : {}),
86
92
  webhook: webhook(razorpayWebhookSecret, options.onWebhookEvent ?? undefined, {
87
93
  subscription: subOpts,
88
94
  onEvent,
package/lib/types.ts CHANGED
@@ -169,6 +169,8 @@ export interface RazorpayPluginOptions {
169
169
  razorpayClient: import('razorpay')
170
170
  /** Webhook secret for signature verification. */
171
171
  razorpayWebhookSecret?: string
172
+ /** API key secret for payment signature verification. When set, enables POST /razorpay/verify-payment (same secret as Razorpay client, not webhook secret). */
173
+ razorpayKeySecret?: string
172
174
  /** Create Razorpay customer when user signs up. Default: false. */
173
175
  createCustomerOnSignUp?: boolean
174
176
  /** Called after a Razorpay customer is created. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deiondz/better-auth-razorpay",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Better Auth plugin for Razorpay subscriptions and payments",
5
5
  "type": "module",
6
6
  "main": "./index.ts",