recur-skills 0.0.9 → 0.0.11

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
3
  "name": "recur-skills",
4
4
  "description": "Claude Code skills for integrating Recur — Taiwan's subscription payment platform",
5
- "version": "0.0.9",
5
+ "version": "0.0.11",
6
6
  "owner": {
7
7
  "name": "Recur",
8
8
  "email": "hi@recur.tw",
@@ -10,14 +10,14 @@
10
10
  },
11
11
  "metadata": {
12
12
  "description": "Claude Code skills for integrating Recur - Taiwan's subscription payment platform",
13
- "version": "0.0.9"
13
+ "version": "0.0.11"
14
14
  },
15
15
  "plugins": [
16
16
  {
17
17
  "name": "recur-skills",
18
18
  "displayName": "Recur Skills",
19
19
  "description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, portal) and the Recur MCP server for signup, API keys, and products",
20
- "version": "0.0.9",
20
+ "version": "0.0.11",
21
21
  "source": "./",
22
22
  "category": "development",
23
23
  "homepage": "https://docs.recur.tw/guides/skills",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "recur-skills",
4
4
  "displayName": "Recur Skills",
5
- "version": "0.0.9",
5
+ "version": "0.0.11",
6
6
  "description": "Skills for Recur SDK integration (quickstart, checkout, webhooks, entitlements, customer portal) plus the Recur MCP server (https://mcp.recur.tw/) for account signup, API keys, and products.",
7
7
  "author": {
8
8
  "name": "Recur",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recur-skills",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "Claude Code skills and MCP plugin for Recur - Taiwan's subscription payment platform",
5
5
  "keywords": [
6
6
  "recur",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "recur-skills",
4
- "version": "0.0.9",
4
+ "version": "0.0.11",
5
5
  "description": "Skills for Recur SDK integration: quickstart, checkout, webhooks, entitlements, and customer portal. Taiwan subscription billing via PAYUNi.",
6
6
  "author": {
7
7
  "name": "Recur",
@@ -4,109 +4,161 @@ description: Implement Recur checkout flows including embedded, modal, and redir
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.9"
7
+ version: "0.0.11"
8
8
  ---
9
9
 
10
10
  # Recur Checkout Integration
11
11
 
12
- You are helping implement Recur checkout flows. Recur supports multiple checkout modes for different use cases.
12
+ You are helping implement Recur checkout flows. Everything below is checked against the
13
+ `recur-tw` typings (`RecurContextValue`, `UseSubscribeResult`, `CheckoutOptions`,
14
+ `RedirectToCheckoutOptions`, `SubscriptionResult`). Product IDs are CUIDs from
15
+ `list_products` / the dashboard (e.g. `cmfxq8n2a0001l8yz3k5p9t7d`); `prod_xxx` in the
16
+ examples is a placeholder. You can also pass `productSlug`.
13
17
 
14
18
  ## Checkout Modes
15
19
 
16
- | Mode | Best For | User Experience |
17
- |------|----------|-----------------|
18
- | `embedded` | SPA apps | Form renders inline in your page |
19
- | `modal` | Quick purchases | Form appears in a dialog overlay |
20
- | `redirect` | Simple integration | Full page redirect to Recur |
20
+ | Mode | How | Best For |
21
+ |------|-----|----------|
22
+ | **Hosted** (recommended) | `useRecur().redirectToCheckout()` → checkout.recur.tw | Any app, works on localhost |
23
+ | **Modal** | `useSubscribe()` / `useRecur().checkout()` with provider `checkoutMode: 'modal'` | Quick purchases without leaving the page |
24
+ | **Embedded** | same hooks with provider `checkoutMode: 'embedded'` + `containerElementId` | Custom checkout pages |
21
25
 
22
- ## Basic Implementation
26
+ `checkoutMode` lives on `<RecurProvider config>`, not on the call. The provider default is
27
+ `'embedded'`, which throws without `containerElementId` — set `checkoutMode: 'modal'` when
28
+ you want the popup. There is no `mode: 'hosted' | 'modal'` option on any call.
23
29
 
24
- ### Using useRecur Hook
30
+ ## Hosted Checkout (recommended)
31
+
32
+ `isCheckingOut` is only set by `checkout()`; `redirectToCheckout()` creates the session and
33
+ navigates away, so track your own pending flag.
25
34
 
26
35
  ```tsx
36
+ 'use client'
37
+
38
+ import { useState } from 'react'
27
39
  import { useRecur } from 'recur-tw'
28
40
 
29
41
  function CheckoutButton({ productId }: { productId: string }) {
30
- const { checkout, isLoading } = useRecur()
42
+ const { redirectToCheckout } = useRecur()
43
+ const [redirecting, setRedirecting] = useState(false)
31
44
 
32
45
  const handleClick = async () => {
33
- await checkout({
34
- productId,
35
- // Or use productSlug: 'pro-plan'
36
-
37
- // Optional: Pre-fill customer info
38
- customerEmail: 'user@example.com',
39
- customerName: 'John Doe',
40
-
41
- // Optional: Link to your user system
42
- externalCustomerId: 'user_123',
43
-
44
- // Callbacks
45
- onPaymentComplete: (result) => {
46
- console.log('Success!', result)
47
- // result.id - Subscription/Order ID
48
- // result.status - 'ACTIVE', 'TRIALING', etc.
49
- },
50
- onPaymentFailed: (error) => {
51
- console.error('Failed:', error)
52
- return { action: 'retry' } // or 'close' or 'custom'
53
- },
54
- onPaymentCancel: () => {
55
- console.log('User cancelled')
56
- },
57
- })
46
+ setRedirecting(true)
47
+ try {
48
+ await redirectToCheckout({
49
+ productId, // or productSlug: 'pro-plan'
50
+ successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
51
+ cancelUrl: `${window.location.origin}/pricing`,
52
+ customerEmail: 'user@example.com', // optional, pre-fills the checkout page
53
+ customerName: 'John Doe', // optional
54
+ externalCustomerId: 'user_123', // optional, links to your user system
55
+ })
56
+ } catch (err) {
57
+ setRedirecting(false)
58
+ console.error('Failed to start checkout:', err)
59
+ }
58
60
  }
59
61
 
60
62
  return (
61
- <button onClick={handleClick} disabled={isLoading}>
62
- {isLoading ? 'Processing...' : 'Subscribe'}
63
+ <button onClick={handleClick} disabled={redirecting}>
64
+ {redirecting ? 'Redirecting...' : 'Subscribe'}
63
65
  </button>
64
66
  )
65
67
  }
66
68
  ```
67
69
 
68
- ### Using useSubscribe Hook (with state management)
70
+ The browser navigates away; no callbacks fire. Confirm the payment on your success page
71
+ (via the session id) or with webhooks.
72
+
73
+ ## Modal / Embedded Checkout
74
+
75
+ ### Using useSubscribe (recommended for modal)
76
+
77
+ Callbacks are options of the hook; `subscribe()` takes the checkout options only.
69
78
 
70
79
  ```tsx
80
+ 'use client'
81
+
71
82
  import { useSubscribe } from 'recur-tw'
83
+ import { useRouter } from 'next/navigation'
72
84
 
73
85
  function SubscribeButton({ productId }: { productId: string }) {
74
- const { subscribe, isLoading, error, subscription } = useSubscribe()
75
-
76
- const handleClick = () => {
77
- subscribe({
78
- productId,
79
- onPaymentComplete: (sub) => {
80
- // Subscription created successfully
81
- router.push('/dashboard')
82
- },
83
- })
84
- }
85
-
86
- if (subscription) {
87
- return <p>Subscribed! ID: {subscription.id}</p>
88
- }
86
+ const router = useRouter()
87
+ const { subscribe, isLoading, error, reset } = useSubscribe({
88
+ onPaymentComplete: (subscription) => {
89
+ // subscription: SubscriptionResult — id, status, productId, amount, currentPeriodEnd
90
+ router.push('/dashboard')
91
+ },
92
+ onPaymentFailed: (error) => {
93
+ console.error('Failed:', error.code, error.message)
94
+ return { action: 'retry' } // or 'close' or 'custom'
95
+ },
96
+ onPaymentCancel: () => console.log('User cancelled'),
97
+ })
89
98
 
90
99
  return (
91
100
  <>
92
- <button onClick={handleClick} disabled={isLoading}>
93
- Subscribe
101
+ <button
102
+ onClick={() => subscribe({ productId, customerEmail: 'user@example.com' })}
103
+ disabled={isLoading}
104
+ >
105
+ {isLoading ? 'Processing...' : 'Subscribe'}
94
106
  </button>
95
- {error && <p className="error">{error.message}</p>}
107
+ {error && <p className="error" onClick={reset}>{error.message}</p>}
96
108
  </>
97
109
  )
98
110
  }
99
111
  ```
100
112
 
101
- ## Embedded Mode Setup
113
+ `useSubscribe()` returns `{ subscribe, mutate, isLoading, error, reset }` — there is no
114
+ `subscription` field; use `onPaymentComplete` for the result.
115
+
116
+ ### Using useRecur().checkout (callbacks inline)
117
+
118
+ ```tsx
119
+ 'use client'
120
+
121
+ import { useRecur } from 'recur-tw'
122
+
123
+ function CheckoutButton({ productId }: { productId: string }) {
124
+ const { checkout, isCheckingOut } = useRecur()
125
+
126
+ const handleClick = () =>
127
+ checkout({
128
+ productId,
129
+ customerEmail: 'user@example.com', // optional; collected in the form if omitted
130
+ customerName: 'John Doe',
131
+ externalCustomerId: 'user_123',
132
+ onPaymentComplete: (subscription) => console.log('Success!', subscription.id, subscription.status),
133
+ onPaymentFailed: (error) => ({ action: 'retry' }),
134
+ onPaymentCancel: () => console.log('User cancelled'),
135
+ })
136
+
137
+ return (
138
+ <button onClick={handleClick} disabled={isCheckingOut}>
139
+ {isCheckingOut ? 'Processing...' : 'Subscribe'}
140
+ </button>
141
+ )
142
+ }
143
+ ```
102
144
 
103
- For embedded mode, you need a container element:
145
+ ### Provider setup for modal / embedded
104
146
 
105
147
  ```tsx
106
- // In RecurProvider config
148
+ // Modal (popup)
149
+ <RecurProvider
150
+ config={{
151
+ publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
152
+ checkoutMode: 'modal',
153
+ }}
154
+ >
155
+ {children}
156
+ </RecurProvider>
157
+
158
+ // Embedded (inline form) — the container must exist in the DOM
107
159
  <RecurProvider
108
160
  config={{
109
- publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY,
161
+ publishableKey: process.env.NEXT_PUBLIC_RECUR_PUBLISHABLE_KEY!,
110
162
  checkoutMode: 'embedded',
111
163
  containerElementId: 'recur-checkout-container',
112
164
  }}
@@ -114,47 +166,76 @@ For embedded mode, you need a container element:
114
166
  {children}
115
167
  </RecurProvider>
116
168
 
117
- // In your checkout page
118
169
  function CheckoutPage() {
119
170
  return (
120
171
  <div>
121
172
  <h1>Complete Your Purchase</h1>
122
- {/* Recur will render the payment form here */}
173
+ {/* Recur renders the payment form here */}
123
174
  <div id="recur-checkout-container" />
124
175
  </div>
125
176
  )
126
177
  }
127
178
  ```
128
179
 
129
- ## Handling 3D Verification
180
+ Modal/embedded checkout needs a registered domain; on `localhost` use Hosted Checkout.
130
181
 
131
- Recur handles 3D Secure automatically. For mobile apps or specific flows:
182
+ ## Option Types
132
183
 
133
- ```tsx
134
- await checkout({
135
- productId,
136
- // These URLs are used when 3D verification requires redirect
137
- successUrl: 'https://yourapp.com/checkout/success',
138
- cancelUrl: 'https://yourapp.com/checkout/cancel',
139
- })
140
- ```
184
+ ```typescript
185
+ // redirectToCheckout()
186
+ interface RedirectToCheckoutOptions {
187
+ productId?: string // or productSlug — one is required
188
+ productSlug?: string
189
+ mode?: 'PAYMENT' | 'SUBSCRIPTION' | 'SETUP' // usually inferred from the product
190
+ successUrl: string
191
+ cancelUrl: string
192
+ customerEmail?: string
193
+ customerName?: string
194
+ externalCustomerId?: string
195
+ }
141
196
 
142
- ## Product Types
197
+ // checkout() / subscribe()
198
+ interface CheckoutOptions {
199
+ productId?: string
200
+ productSlug?: string
201
+ customerEmail?: string
202
+ customerName?: string
203
+ externalCustomerId?: string
204
+ successUrl?: string // used if 3-D Secure needs a redirect
205
+ cancelUrl?: string
206
+ // checkout() only — for subscribe() pass these to useSubscribe():
207
+ onPaymentComplete?: (subscription: SubscriptionResult) => void
208
+ onPaymentFailed?: (error: CheckoutError) => PaymentFailedAction | void
209
+ onPaymentCancel?: () => void
210
+ onSuccess?: (result: CheckoutResult) => void // session created (before payment)
211
+ onError?: (error: CheckoutError) => void
212
+ }
143
213
 
144
- Recur supports different product types:
214
+ interface SubscriptionResult { // onPaymentComplete argument
215
+ id: string
216
+ status: string // 'ACTIVE', 'TRIALING', ...
217
+ productId: string
218
+ amount: number // whole TWD (499 = NT$499). NEVER divide by 100
219
+ billingPeriod: string
220
+ currentPeriodStart: string
221
+ currentPeriodEnd: string
222
+ trialEndsAt?: string
223
+ nextBillingDate?: string
224
+ }
225
+ ```
145
226
 
146
- ```tsx
147
- // Subscription (recurring)
148
- checkout({ productId: 'prod_subscription_xxx' })
227
+ No `trialDays`, `quantity`, or `metadata` on these options — trials and pricing come from
228
+ the product (the server SDK's `checkoutSessions.create()` does accept `metadata`).
149
229
 
150
- // One-time purchase
151
- checkout({ productId: 'prod_onetime_xxx' })
230
+ ## Product Types
152
231
 
153
- // Credits (prepaid wallet)
154
- checkout({ productId: 'prod_credits_xxx' })
232
+ All four types go through the same calls; the product decides the behaviour:
155
233
 
156
- // Donation (variable amount)
157
- checkout({ productId: 'prod_donation_xxx' })
234
+ ```tsx
235
+ redirectToCheckout({ productSlug: 'pro-monthly', successUrl, cancelUrl }) // SUBSCRIPTION (recurring)
236
+ redirectToCheckout({ productSlug: 'ebook', successUrl, cancelUrl }) // ONE_TIME
237
+ redirectToCheckout({ productSlug: 'credits-100', successUrl, cancelUrl }) // CREDITS (prepaid wallet)
238
+ redirectToCheckout({ productSlug: 'support-us', successUrl, cancelUrl }) // DONATION (variable amount)
158
239
  ```
159
240
 
160
241
  ## Listing Products
@@ -163,15 +244,14 @@ checkout({ productId: 'prod_donation_xxx' })
163
244
  import { useProducts } from 'recur-tw'
164
245
 
165
246
  function PricingPage() {
166
- const { products, isLoading } = useProducts({
167
- type: 'SUBSCRIPTION', // Filter by type
168
- })
247
+ const { data: products, isLoading, error } = useProducts({ type: 'SUBSCRIPTION' })
169
248
 
170
249
  if (isLoading) return <div>Loading...</div>
250
+ if (error) return <div>{error.message}</div>
171
251
 
172
252
  return (
173
253
  <div className="pricing-grid">
174
- {products.map(product => (
254
+ {products?.map((product) => (
175
255
  <PricingCard key={product.id} product={product} />
176
256
  ))}
177
257
  </div>
@@ -179,36 +259,56 @@ function PricingPage() {
179
259
  }
180
260
  ```
181
261
 
262
+ `Product` fields: `id`, `name`, `slug`, `description`, `type`, `billingPeriod`
263
+ (`'MONTHLY' | 'YEARLY' | ... | null`), `price` (whole TWD), `currency`, `trialDays`,
264
+ `metadata`. There is no `priceFormatted` on the SDK type — format with
265
+ `` `NT$${product.price.toLocaleString()}` ``.
266
+
182
267
  ## Payment Failed Handling
183
268
 
269
+ `CheckoutError.code` is `'PAYMENT_FAILED'` for a declined payment; the gateway's own code
270
+ is in `error.details.failure_code` (with `failure_message` and `can_retry`).
271
+ `CheckoutErrorDetails` is a union — an array for conflict errors, an object for payment
272
+ failures — so narrow it before reading the fields.
273
+
184
274
  ```tsx
185
275
  onPaymentFailed: (error) => {
186
- // error.code tells you what went wrong
187
- switch (error.code) {
276
+ const details = Array.isArray(error.details) ? undefined : error.details
277
+
278
+ switch (details?.failure_code) {
188
279
  case 'CARD_DECLINED':
189
280
  return { action: 'retry' }
190
281
  case 'INSUFFICIENT_FUNDS':
191
- return {
192
- action: 'custom',
193
- customTitle: '餘額不足',
194
- customMessage: '請使用其他付款方式',
195
- }
282
+ return { action: 'custom', customTitle: '餘額不足', customMessage: '請使用其他付款方式' }
196
283
  default:
197
- return { action: 'close' }
284
+ return details?.can_retry ? { action: 'retry' } : { action: 'close' }
198
285
  }
199
286
  }
200
287
  ```
201
288
 
202
- ## Server-Side Checkout (API)
289
+ ## Server-Side Checkout (Hosted, no React)
290
+
291
+ ```typescript
292
+ import { Recur } from 'recur-tw/server'
293
+
294
+ const recur = new Recur(process.env.RECUR_SECRET_KEY!)
295
+ const session = await recur.checkoutSessions.create({
296
+ productId: 'prod_xxx', // a CUID from list_products
297
+ successUrl: 'https://yourapp.com/success',
298
+ cancelUrl: 'https://yourapp.com/cancel',
299
+ customerEmail: 'user@example.com',
300
+ metadata: { plan: 'pro' }, // optional, server-side only
301
+ })
302
+ // redirect the customer to session.url
303
+ ```
203
304
 
204
- For server-rendered apps or custom flows:
305
+ Or with plain REST:
205
306
 
206
307
  ```typescript
207
- // Create checkout session
208
- const response = await fetch('https://api.recur.tw/v1/checkouts', {
308
+ const response = await fetch('https://api.recur.tw/v1/checkout/sessions', {
209
309
  method: 'POST',
210
310
  headers: {
211
- 'X-Recur-Secret-Key': process.env.RECUR_SECRET_KEY,
311
+ Authorization: `Bearer ${process.env.RECUR_SECRET_KEY}`, // X-Recur-Secret-Key also works
212
312
  'Content-Type': 'application/json',
213
313
  },
214
314
  body: JSON.stringify({
@@ -219,31 +319,19 @@ const response = await fetch('https://api.recur.tw/v1/checkouts', {
219
319
  }),
220
320
  })
221
321
 
222
- const { checkoutUrl } = await response.json()
223
- // Redirect user to checkoutUrl
224
- ```
225
-
226
- ## Checkout Result Structure
227
-
228
- ```typescript
229
- interface CheckoutResult {
230
- id: string // Subscription or Order ID
231
- status: string // 'ACTIVE', 'TRIALING', 'PENDING'
232
- productId: string
233
- amount: number // In cents (e.g., 29900 = NT$299)
234
- billingPeriod?: string // 'MONTHLY', 'YEARLY' for subscriptions
235
- currentPeriodEnd?: string // ISO date
236
- trialEndsAt?: string // ISO date if trial
237
- }
322
+ // The session object is the response body itself (no outer `data` wrapper, snake_case keys)
323
+ const { url } = await response.json()
324
+ // Redirect the customer to `url`. (/v1/checkouts is the embedded-form endpoint, different shape.)
238
325
  ```
239
326
 
240
327
  ## Best Practices
241
328
 
242
- 1. **Always handle all callbacks** - onPaymentComplete, onPaymentFailed, onPaymentCancel
243
- 2. **Show loading states** - Use isLoading to disable buttons during checkout
244
- 3. **Pre-fill customer info** - Reduces friction if you already have user data
245
- 4. **Use externalCustomerId** - Links Recur customers to your user system
246
- 5. **Test in sandbox first** - Use `pk_test_` keys during development
329
+ 1. **Default to Hosted Checkout** — works everywhere, including localhost
330
+ 2. **Handle every callback** on modal/embedded — onPaymentComplete, onPaymentFailed, onPaymentCancel
331
+ 3. **Show loading states** — `isCheckingOut` (useRecur, modal/embedded only) / `isLoading` (useSubscribe); for `redirectToCheckout()` track your own flag
332
+ 4. **Pre-fill customer info** when you already have it; it is optional
333
+ 5. **Use externalCustomerId** to link Recur customers to your user system
334
+ 6. **Test in sandbox first** — `pk_test_` / `sk_test_` keys
247
335
 
248
336
  ## Related Skills
249
337
 
@@ -4,7 +4,7 @@ description: Implement access control and permission checking with Recur entitle
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.9"
7
+ version: "0.0.11"
8
8
  ---
9
9
 
10
10
  # Recur Entitlements & Access Control
@@ -71,7 +71,7 @@ const { check } = useCustomer()
71
71
  // Check by product slug
72
72
  const { allowed, entitlement } = check('pro-plan')
73
73
 
74
- // Check by product ID
74
+ // Check by product ID (a CUID from list_products, e.g. 'cmfxq8n2a0001l8yz3k5p9t7d'; 'prod_xxx' is a placeholder)
75
75
  const { allowed } = check('prod_xxx')
76
76
 
77
77
  if (allowed) {
@@ -111,14 +111,24 @@ onPaymentComplete: async () => {
111
111
  ## Entitlement Response Structure
112
112
 
113
113
  ```typescript
114
+ interface CheckResult { // what check() returns
115
+ allowed: boolean
116
+ reason?: 'no_customer' | 'no_entitlement' | 'not_found' | 'expired' | 'insufficient_balance'
117
+ entitlement?: Entitlement
118
+ balance?: number // CREDITS products
119
+ unlimited?: boolean
120
+ subscription?: { id: string; status: string; product: { id: string; slug: string; name: string }; currentPeriodEnd: string }
121
+ }
122
+
114
123
  interface Entitlement {
115
124
  product: string // Product slug
116
- productId: string // Product ID
125
+ productId: string // Product ID (CUID)
117
126
  status: EntitlementStatus
118
127
  source: 'subscription' | 'order' // How they got access
119
128
  sourceId: string // Subscription/Order ID
120
129
  grantedAt: string // When access was granted
121
130
  expiresAt: string | null // When access expires (null = permanent)
131
+ subscriptionId?: string
122
132
  }
123
133
 
124
134
  type EntitlementStatus =
@@ -140,7 +150,9 @@ const recur = new Recur(process.env.RECUR_SECRET_KEY!)
140
150
 
141
151
  // In API route or server action
142
152
  async function checkAccess(userEmail: string) {
143
- const { allowed, entitlement } = await recur.entitlements.check({
153
+ // Server EntitlementCheckResult is { allowed, subscription? } — there is no
154
+ // `entitlement` field here (that one belongs to the React check()).
155
+ const { allowed, subscription } = await recur.entitlements.check({
144
156
  product: 'pro-plan',
145
157
  customer: { email: userEmail },
146
158
  })
@@ -149,7 +161,7 @@ async function checkAccess(userEmail: string) {
149
161
  throw new Error('Upgrade required')
150
162
  }
151
163
 
152
- return entitlement
164
+ return subscription
153
165
  }
154
166
  ```
155
167
 
@@ -166,6 +178,9 @@ const response = await fetch(
166
178
  }
167
179
  )
168
180
 
181
+ // REST responses are snake_case (the API wrapper converts them), unlike the SDK's
182
+ // camelCase types: customer { id, email, name, external_id }, subscription | null,
183
+ // entitlements[] with product_id / granted_at / expires_at / source_id.
169
184
  const { customer, subscription, entitlements } = await response.json()
170
185
  ```
171
186
 
@@ -254,16 +269,13 @@ export async function requireSubscription(
254
269
  ) {
255
270
  const userEmail = await getUserEmail(req) // Your auth logic
256
271
 
257
- const { allowed, denial } = await recur.entitlements.check({
272
+ const { allowed } = await recur.entitlements.check({
258
273
  product,
259
274
  customer: { email: userEmail },
260
275
  })
261
276
 
262
277
  if (!allowed) {
263
- throw new Response(JSON.stringify({
264
- error: 'Subscription required',
265
- reason: denial?.reason, // 'no_customer', 'no_entitlement', etc.
266
- }), {
278
+ throw new Response(JSON.stringify({ error: 'Subscription required' }), {
267
279
  status: 403,
268
280
  headers: { 'Content-Type': 'application/json' },
269
281
  })
@@ -349,13 +361,13 @@ if (entitlement?.status === 'canceled') {
349
361
 
350
362
  ## Denial Reasons
351
363
 
352
- When `allowed` is `false`, check the denial reason:
364
+ When `allowed` is `false`, `CheckResult.reason` says why (`'no_customer' | 'no_entitlement' | 'not_found' | 'expired' | 'insufficient_balance'`):
353
365
 
354
366
  ```typescript
355
- const { allowed, denial } = check('pro-plan')
367
+ const { allowed, reason } = check('pro-plan')
356
368
 
357
369
  if (!allowed) {
358
- switch (denial?.reason) {
370
+ switch (reason) {
359
371
  case 'no_customer':
360
372
  // Customer not found
361
373
  return <CreateAccountPrompt />
@@ -4,7 +4,7 @@ description: List all available Recur skills and how to use them. Use when user
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.9"
7
+ version: "0.0.11"
8
8
  ---
9
9
 
10
10
  # Recur Skills 使用指南
@@ -4,7 +4,7 @@ description: Implement Customer Portal for subscription self-service. Use when b
4
4
  license: MIT
5
5
  metadata:
6
6
  author: recur
7
- version: "0.0.9"
7
+ version: "0.0.11"
8
8
  ---
9
9
 
10
10
  # Recur Customer Portal Integration