@waffo/pancake-ts 0.1.7 → 0.2.0

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  TypeScript SDK for the Waffo Pancake Merchant of Record (MoR) payment platform.
4
4
 
5
- - Zero runtime dependencies, ESM-only, Node >= 18
5
+ - Zero runtime dependencies, ESM + CJS, Node >= 20
6
6
  - Automatic RSA-SHA256 request signing with deterministic idempotency keys
7
7
  - Full TypeScript type definitions (15 enums, 40+ interfaces)
8
8
  - Webhook verification with embedded public keys (test/prod)
@@ -15,258 +15,305 @@ npm install @waffo/pancake-ts
15
15
 
16
16
  ## Quick Start
17
17
 
18
+ > Most merchants create stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The SDK is primarily used for **checkout integration** — redirecting buyers from your site to the Waffo checkout page.
19
+
18
20
  ```typescript
19
21
  import { WaffoPancake } from "@waffo/pancake-ts";
20
22
 
23
+ // Merchant ID and API Key are available in Dashboard > Settings > Developers
21
24
  const client = new WaffoPancake({
22
- merchantId: process.env.WAFFO_MERCHANT_ID!,
25
+ merchantId: process.env.WAFFO_MERCHANT_ID!, // MER_{base62} format
23
26
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
24
27
  });
25
28
 
26
- // Create a store
27
- const { store } = await client.stores.create({ name: "My Store" });
28
-
29
- // Create a one-time product with multi-currency pricing
30
- const { product } = await client.onetimeProducts.create({
31
- storeId: store.id,
32
- name: "E-Book: TypeScript Handbook",
33
- prices: {
34
- USD: { amount: 2900, taxCategory: "digital_goods" },
35
- EUR: { amount: 2700, taxCategory: "digital_goods" },
36
- },
37
- });
38
-
39
- // Create a checkout session and redirect the buyer
40
- const session = await client.checkout.createSession({
41
- storeId: store.id,
42
- productId: product.id,
29
+ // Create a checkout session — one call handles token + session + URL
30
+ const result = await client.checkout.authenticated.create({
31
+ storeId: "STO_xxx", // from Dashboard > Stores
32
+ productId: "PROD_xxx", // from Dashboard > Products
43
33
  productType: "onetime",
44
34
  currency: "USD",
35
+ buyerIdentity: req.user.email, // your user's identity
45
36
  });
46
- // => redirect buyer to session.checkoutUrl
47
37
 
48
- // Query data via GraphQL (Query only, no Mutations)
49
- const result = await client.graphql.query<{ stores: Array<{ id: string; name: string }> }>({
50
- query: `query { stores { id name status } }`,
51
- });
38
+ // Redirect buyer to the checkout page (opens in new tab)
39
+ res.json({ checkoutUrl: result.checkoutUrl });
40
+ // => checkoutUrl includes #token=... (form pre-filled)
52
41
  ```
53
42
 
54
43
  ## Configuration
55
44
 
56
45
  | Parameter | Type | Required | Description |
57
46
  |-----------|------|----------|-------------|
58
- | `merchantId` | `string` | Yes | Merchant ID, sent as `X-Merchant-Id` header |
59
- | `privateKey` | `string` | Yes | RSA private key (see [Private Key Formats](#private-key-formats) below) |
60
- | `baseUrl` | `string` | No | API base URL (default: `https://waffo-pancake-auth-service.vercel.app`) |
47
+ | `merchantId` | `string` | Yes | Merchant ID in `MER_{base62}` format |
48
+ | `privateKey` | `string` | Yes | RSA private key in PEM format (auto-normalized, see [docs](docs/api-reference.md)) |
49
+ | `baseUrl` | `string` | No | API base URL override |
61
50
  | `fetch` | `typeof fetch` | No | Custom fetch implementation |
62
- | `webhookPublicKey` | `string \| { test?, prod? }` | No | Custom webhook public key(s) (see [Webhook Public Key Resolution](#webhook-public-key-resolution) below) |
51
+ | `webhookPublicKey` | `string \| { test?, prod? }` | No | Custom webhook public key(s) |
52
+
53
+ The SDK auto-normalizes key formats: standard PEM, PKCS#1, literal `\n` from env vars, raw base64, and Windows line endings are all accepted.
54
+
55
+ ## Checkout Integration
56
+
57
+ Waffo supports two checkout modes based on whether the merchant knows the buyer's identity:
58
+
59
+ - **Merchants with their own sites** know who the buyer is — they have user accounts, login systems, or collect buyer info before checkout. The merchant provides the buyer's identity upfront, and the checkout form arrives pre-filled.
60
+ - **Template stores and shared links** have no prior buyer context — the buyer arrives directly at the checkout page and fills in their own details.
63
61
 
64
- ### Private Key Formats
62
+ | Mode | Method | Buyer Identity | Form State | Use Case |
63
+ |------|--------|---------------|------------|----------|
64
+ | **Authenticated** | `checkout.authenticated.create()` | Merchant provides | Pre-filled | Merchant sites with user accounts |
65
+ | **Anonymous** | `checkout.anonymous.create()` | Not provided | Empty | Template stores, one-time purchase links |
65
66
 
66
- The SDK automatically normalizes `privateKey` at construction time, so all of the following formats are accepted:
67
+ > **We recommend authenticated checkout whenever possible.** The most important reason: authenticated checkout binds the order to the `buyerIdentity` you provide, which is a **merchant-controlled stable identifier**. Even if the buyer changes the email on the checkout form, the order is still tied to the identity you specified. In anonymous mode, the buyer self-reports their email on the form — if they enter a different address, the system treats them as a new user, which means **previous orders become unlinked** and **subscription trial periods can be exploited** (a new email = a new user = a fresh trial).
68
+ >
69
+ > | | Authenticated | Anonymous |
70
+ > |---|---|---|
71
+ > | **Identity** | Merchant-provided, stable across orders | Self-reported email, may vary |
72
+ > | **Form** | Pre-filled from merchant-provided identity | Empty, buyer fills manually |
73
+ > | **Post-purchase** | Full self-service (see [Buyer Self-Service](#buyer-self-service)) | Create orders only — no post-purchase self-service |
74
+ > | **Session** | 5-minute TTL, auto-refreshes | 1-minute, single-use |
67
75
 
68
- | Format | Example | Notes |
69
- |--------|---------|-------|
70
- | Standard PKCS#8 PEM | `-----BEGIN PRIVATE KEY-----\n...` | Recommended |
71
- | PKCS#1 PEM | `-----BEGIN RSA PRIVATE KEY-----\n...` | Also accepted |
72
- | Literal `\n` (env vars) | `"-----BEGIN PRIVATE KEY-----\\nMIIE..."` | Common when stored in `.env` or CI secrets |
73
- | Windows line endings | `\r\n` | Converted to `\n` |
74
- | Raw base64 (no headers) | `MIIEvQIBADANBgkqhki...` | Wrapped with PKCS#8 headers automatically |
75
- | Single-line base64 with headers | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
76
+ Both modes support **dynamic pricing** and **trial control** at checkout time:
76
77
 
77
- If the key is invalid or empty, the constructor throws a descriptive error immediately rather than failing silently on the first API call.
78
+ - `priceSnapshot` override the product's stored price with a custom amount (e.g., coupon, volume discount)
79
+ - `withTrial` — explicitly enable or disable the trial period for subscriptions (`true` = force trial, `false` = skip trial, omit = use default rules)
80
+
81
+ ### Authenticated Checkout (Recommended)
82
+
83
+ The merchant provides buyer identity — the SDK issues a session token, creates a checkout session, and returns a checkout URL with the token appended as a URL fragment. One call does everything.
78
84
 
79
85
  ```typescript
80
- // All of these work:
81
- new WaffoPancake({ merchantId: "m_1", privateKey: process.env.PRIVATE_KEY! }); // .env with literal \n
82
- new WaffoPancake({ merchantId: "m_1", privateKey: fs.readFileSync("key.pem", "utf8") }); // file read
83
- new WaffoPancake({ merchantId: "m_1", privateKey: rawBase64String }); // raw base64
86
+ // Basic buyer identity only
87
+ const result = await client.checkout.authenticated.create({
88
+ storeId: "STO_xxx",
89
+ productId: "PROD_xxx",
90
+ productType: "onetime",
91
+ currency: "USD",
92
+ buyerIdentity: "customer@example.com",
93
+ });
94
+
95
+ // With dynamic pricing — override stored price (e.g., coupon, volume discount)
96
+ const result = await client.checkout.authenticated.create({
97
+ storeId: "STO_xxx",
98
+ productId: "PROD_xxx",
99
+ productType: "onetime",
100
+ currency: "USD",
101
+ buyerIdentity: "customer@example.com",
102
+ priceSnapshot: { amount: "19.99", taxCategory: "digital_goods" },
103
+ });
104
+
105
+ // Subscription with trial control + billing detail pre-fill
106
+ const result = await client.checkout.authenticated.create({
107
+ storeId: "STO_xxx",
108
+ productId: "PROD_xxx",
109
+ productType: "subscription",
110
+ currency: "USD",
111
+ buyerIdentity: "customer@example.com",
112
+ withTrial: true, // force enable trial (false = skip, omit = default rules)
113
+ billingDetail: { country: "US", isBusiness: false },
114
+ });
115
+
116
+ // result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}"
117
+ window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
84
118
  ```
85
119
 
86
- ### Webhook Public Key Resolution
120
+ The token is passed via the URL fragment (after `#`), which is never sent to the server and never appears in the `Referer` header.
87
121
 
88
- The SDK resolves the webhook verification public key per environment using a multi-level fallback chain:
122
+ ### Anonymous Checkout
89
123
 
90
- | Priority | Source | Description |
91
- |----------|--------|-------------|
92
- | 1 | `options.publicKey` | Per-call override (highest priority, skips all resolution) |
93
- | 2 | `config.webhookPublicKey[env]` | Config object per-environment key |
94
- | 3 | `config.webhookPublicKey` (string) | Config shared key (both environments) |
95
- | 4 | `WAFFO_WEBHOOK_TEST_PUBLIC_KEY` / `WAFFO_WEBHOOK_PROD_PUBLIC_KEY` | Environment variable per-environment |
96
- | 5 | `WAFFO_WEBHOOK_PUBLIC_KEY` | Environment variable shared |
97
- | 6 | Built-in hardcoded key | SDK-embedded Waffo public key (default) |
124
+ No buyer identity required the buyer fills in billing details manually on the checkout page.
98
125
 
99
126
  ```typescript
100
- // Shared key for both environments
101
- new WaffoPancake({ merchantId: "m_1", privateKey: "...", webhookPublicKey: "MIIBIjAN..." });
127
+ const result = await client.checkout.anonymous.create({
128
+ storeId: "STO_xxx",
129
+ productId: "PROD_xxx",
130
+ productType: "onetime",
131
+ currency: "USD",
132
+ });
102
133
 
103
- // Per-environment keys
104
- new WaffoPancake({
105
- merchantId: "m_1",
106
- privateKey: "...",
107
- webhookPublicKey: {
108
- test: process.env.WAFFO_TEST_PUB_KEY!,
109
- prod: process.env.WAFFO_PROD_PUB_KEY!,
110
- },
134
+ // Also supports priceSnapshot and withTrial
135
+ const result = await client.checkout.anonymous.create({
136
+ storeId: "STO_xxx",
137
+ productId: "PROD_xxx",
138
+ productType: "subscription",
139
+ currency: "USD",
140
+ priceSnapshot: { amount: "4.99", taxCategory: "saas" },
141
+ withTrial: false, // skip trial for this session
111
142
  });
112
143
 
113
- // Or rely on environment variables (no config needed)
114
- // export WAFFO_WEBHOOK_TEST_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
115
- // export WAFFO_WEBHOOK_PROD_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."
116
- new WaffoPancake({ merchantId: "m_1", privateKey: "..." });
117
- // => SDK auto-reads from env vars, falls back to built-in keys
144
+ window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
118
145
  ```
119
146
 
120
- ### Public Key Formats
147
+ ### Opening the Checkout Page
121
148
 
122
- All public key inputs (config, env vars, per-call) accept the same flexible formats as private keys:
149
+ **We recommend opening the checkout page in a new tab** rather than navigating in the current page:
123
150
 
124
- | Format | Example | Notes |
125
- |--------|---------|-------|
126
- | Standard SPKI PEM | `-----BEGIN PUBLIC KEY-----\n...` | Recommended |
127
- | PKCS#1 PEM | `-----BEGIN RSA PUBLIC KEY-----\n...` | Also accepted |
128
- | Literal `\n` (env vars) | `"-----BEGIN PUBLIC KEY-----\\nMIIB..."` | Common when stored in `.env` or CI secrets |
129
- | Windows line endings | `\r\n` | Converted to `\n` |
130
- | Raw base64 (no headers) | `MIIBIjANBgkqhki...` | Wrapped with SPKI headers automatically |
131
- | Single-line base64 with headers | Header + all base64 on one line + footer | Re-wrapped to 64-char lines |
151
+ - Buyers can return to your site immediately after payment or if they close the checkout tab
152
+ - Merchant page state (cart, forms, scroll position) is preserved
153
+ - Payment flow is decoupled from the browsing experience, reducing checkout abandonment
132
154
 
133
- ## Resources
155
+ ```typescript
156
+ // Recommended: open in a new tab
157
+ window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
134
158
 
135
- | Namespace | Methods | Description |
136
- |-----------|---------|-------------|
137
- | `client.auth` | `issueSessionToken()` | Issue a buyer session token (JWT) |
138
- | `client.stores` | `create()` `update()` `delete()` | Store management (webhook, notification, checkout settings) |
139
- | `client.storeMerchants` | `add()` `remove()` `updateRole()` | Store member management (coming soon, returns 501) |
140
- | `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time product CRUD with multi-currency pricing and version management |
141
- | `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription product CRUD with billing period and version management |
142
- | `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups for shared trial and plan switching |
143
- | `client.orders` | `cancelSubscription()` | Order management (pending→canceled, active→canceling) |
144
- | `client.checkout` | `createSession()` | Create a checkout session with trial toggle, billing detail, and price snapshot |
145
- | `client.graphql` | `query<T>()` | Typed GraphQL queries (Query only, no Mutations) |
146
- | `client.webhooks` | `verify<T>()` | Webhook signature verification (uses configured `webhookPublicKey` or built-in keys) |
147
-
148
- See [API Reference](docs/api-reference.md) for complete parameter tables and return types.
159
+ // Or via an <a> tag
160
+ // <a href={checkoutUrl} target="_blank" rel="noopener noreferrer">Proceed to Checkout</a>
161
+ ```
149
162
 
150
- ## Checkout Integration
163
+ > **Not recommended:** `window.location.href = result.checkoutUrl` replaces the current page, preventing buyers from returning to your site without browser back navigation.
151
164
 
152
- Guide buyers from your site to the Waffo checkout page in three steps:
165
+ See [API Reference Checkout](docs/api-reference.md#checkout) for full parameter tables and `BillingDetail` field requirements.
153
166
 
154
- ```
155
- 1. Issue Session Token → Obtain a buyer identity credential (JWT)
156
- 2. Create Checkout Session → Create a session and get the checkout URL
157
- 3. Open Checkout Page → Open the checkout in a new browser tab
158
- ```
167
+ ## Webhook Verification
159
168
 
160
- ### Step 1 Issue a Session Token
169
+ After a buyer completes payment, Waffo sends webhook events to your server. The SDK provides two ways to verify signatures:
161
170
 
162
- Your backend requests a Session Token on behalf of the buyer. The token carries the buyer's identity and is used by the checkout page to load order details and place orders.
171
+ ### Standalone Function (built-in keys)
163
172
 
164
173
  ```typescript
165
- const { token } = await client.auth.issueSessionToken({
166
- storeId: "store_xxx",
167
- buyerIdentity: "customer@example.com",
174
+ import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
175
+
176
+ // Express (IMPORTANT: use raw body — parsed JSON breaks signature verification)
177
+ app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
178
+ try {
179
+ const event = verifyWebhook(
180
+ req.body.toString("utf-8"),
181
+ req.headers["x-waffo-signature"] as string,
182
+ );
183
+
184
+ // Respond immediately, process asynchronously
185
+ res.status(200).send("OK");
186
+
187
+ switch (event.eventType) {
188
+ case WebhookEventType.OrderCompleted:
189
+ console.log(`Order ${event.data.orderId} completed`);
190
+ break;
191
+ case WebhookEventType.SubscriptionActivated:
192
+ console.log(`Subscription activated for ${event.data.buyerEmail}`);
193
+ break;
194
+ }
195
+ } catch {
196
+ res.status(401).send("Invalid signature");
197
+ }
168
198
  });
169
- ```
170
199
 
171
- ### Step 2 — Create a Checkout Session
200
+ // Next.js App Router
201
+ export async function POST(request: Request) {
202
+ const body = await request.text();
203
+ const sig = request.headers.get("x-waffo-signature");
204
+ try {
205
+ const event = verifyWebhook(body, sig);
206
+ return new Response("OK");
207
+ } catch {
208
+ return new Response("Invalid signature", { status: 401 });
209
+ }
210
+ }
211
+ ```
172
212
 
173
- Create a checkout session with your API Key. The response includes a checkout URL with the token embedded in the URL fragment.
213
+ ### Client Instance Method (multi-level key resolution)
174
214
 
175
215
  ```typescript
176
- import { CheckoutSessionProductType } from "@waffo/pancake-ts";
177
-
178
- const session = await client.checkout.createSession({
179
- storeId: "store_xxx",
180
- productId: "prod_xxx",
181
- productType: CheckoutSessionProductType.Onetime,
182
- currency: "USD",
183
- buyerEmail: "customer@example.com",
184
- successUrl: "https://example.com/thank-you",
216
+ const client = new WaffoPancake({
217
+ merchantId: "MER_xxx",
218
+ privateKey: "...",
219
+ webhookPublicKey: {
220
+ test: process.env.WAFFO_TEST_PUB_KEY!,
221
+ prod: process.env.WAFFO_PROD_PUB_KEY!,
222
+ },
185
223
  });
186
- // session.checkoutUrl format:
187
- // https://waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}
224
+ const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
188
225
  ```
189
226
 
190
- The token is passed via the URL fragment (after `#`), which is never sent to the server and never appears in the `Referer` header.
227
+ See [Webhook Guide](docs/webhook-guide.md) for event types, dual-environment key architecture, key resolution chain, retry mechanism, and best practices.
191
228
 
192
- ### Step 3 — Open Checkout Page (New Tab)
229
+ ## Buyer Self-Service
193
230
 
194
- **We recommend opening the checkout page in a new tab** rather than navigating in the current page. Benefits:
231
+ Beyond checkout, you can let buyers manage their own orders and subscriptions — for example, embedding a "Cancel Subscription" or "Request Refund" button in your site.
195
232
 
196
- - Buyers can return to your site immediately after payment or if they close the checkout tab
197
- - Merchant page state (cart, forms, scroll position) is preserved
198
- - Payment flow is decoupled from the browsing experience, reducing checkout abandonment
233
+ Issue a session token, then use `client.buyer(token)` to get a session with self-service methods:
199
234
 
200
235
  ```typescript
201
- // Frontendrecommended: open in a new tab
202
- window.open(session.checkoutUrl, "_blank", "noopener,noreferrer");
236
+ // Your backend issue a session token for the buyer
237
+ const { token } = await client.auth.issueSessionToken({
238
+ storeId: "STO_xxx",
239
+ buyerIdentity: req.user.email,
240
+ });
203
241
 
204
- // Or via an <a> tag
205
- // <a href={checkoutUrl} target="_blank" rel="noopener noreferrer">Proceed to Checkout</a>
206
- ```
242
+ // Create a buyer session
243
+ const buyer = client.buyer(token);
207
244
 
208
- > **Not recommended:** `window.location.href = session.checkoutUrl` replaces the current page, preventing buyers from returning to your site without browser back navigation.
245
+ // Cancel a subscription
246
+ const { orderId, status } = await buyer.cancelSubscription({ orderId: "ORD_xxx" });
247
+ // status: "canceling" (active) or "canceled" (pending)
209
248
 
210
- ### Complete Example (Express)
249
+ // Reactivate a canceled subscription
250
+ await buyer.reactivateSubscription({ orderId: "ORD_xxx" });
211
251
 
212
- ```typescript
213
- import express from "express";
214
- import { WaffoPancake, CheckoutSessionProductType } from "@waffo/pancake-ts";
252
+ // Cancel a one-time order (while payment is pending)
253
+ await buyer.cancelOnetimeOrder({ orderId: "ORD_yyy" });
215
254
 
216
- const client = new WaffoPancake({
217
- merchantId: process.env.WAFFO_MERCHANT_ID!,
218
- privateKey: process.env.WAFFO_PRIVATE_KEY!,
255
+ // Submit a refund request
256
+ const { ticket } = await buyer.createRefundTicket({
257
+ paymentId: "PAY_xxx",
258
+ reason: "Product not as described",
259
+ requestedAmount: { amount: "29.00", currency: "USD" },
219
260
  });
220
261
 
221
- const app = express();
222
-
223
- app.post("/api/checkout", async (req, res) => {
224
- const { productId, currency, buyerEmail } = req.body;
225
-
226
- // Step 1: Issue session token
227
- const { token } = await client.auth.issueSessionToken({
228
- storeId: "store_xxx",
229
- buyerIdentity: buyerEmail,
230
- });
231
-
232
- // Step 2: Create checkout session
233
- const session = await client.checkout.createSession({
234
- storeId: "store_xxx",
235
- productId,
236
- productType: CheckoutSessionProductType.Onetime,
237
- currency,
238
- buyerEmail,
239
- successUrl: "https://example.com/thank-you",
240
- });
241
-
242
- // Return URL to frontend (frontend opens in new tab)
243
- res.json({ checkoutUrl: session.checkoutUrl });
262
+ // Resubmit a rejected refund ticket
263
+ await buyer.resubmitRefundTicket({
264
+ ticketId: "TKT_xxx",
265
+ paymentId: "PAY_xxx",
266
+ reason: "Updated reason with more detail",
267
+ requestedAmount: { amount: "29.00", currency: "USD" },
244
268
  });
245
- ```
246
269
 
247
- ```typescript
248
- // Frontend
249
- const res = await fetch("/api/checkout", {
250
- method: "POST",
251
- headers: { "Content-Type": "application/json" },
252
- body: JSON.stringify({ productId: "prod_xxx", currency: "USD", buyerEmail: "customer@example.com" }),
270
+ // Query the buyer's own orders via GraphQL
271
+ const result = await buyer.graphql.query({
272
+ query: `query { orders { id status createdAt } }`,
253
273
  });
254
- const { checkoutUrl } = await res.json();
255
- window.open(checkoutUrl, "_blank", "noopener,noreferrer");
256
274
  ```
257
275
 
258
- ## Usage Examples
276
+ The token is scoped to the specified store and buyer identity — buyers can only access their own data. Token TTL is 5 minutes and auto-refreshes on each API call.
277
+
278
+ > **Note**: This uses the same `buyerIdentity` as `checkout.authenticated.create()`. Orders placed via authenticated checkout are automatically tied to this identity, so the buyer can manage them later with a token issued here.
259
279
 
260
- ### AuthIssue a Buyer Session Token
280
+ ## GraphQLTyped Queries
261
281
 
262
282
  ```typescript
263
- const { token, expiresAt } = await client.auth.issueSessionToken({
264
- storeId: "store_xxx",
265
- buyerIdentity: "customer@example.com",
283
+ // Simple query
284
+ interface StoresQuery {
285
+ stores: Array<{ id: string; name: string; status: string }>;
286
+ }
287
+ const result = await client.graphql.query<StoresQuery>({
288
+ query: `query { stores { id name status } }`,
289
+ });
290
+
291
+ // Query with variables
292
+ const product = await client.graphql.query({
293
+ query: `query ($id: ID!) { onetimeProduct(id: $id) { id name prices } }`,
294
+ variables: { id: "PROD_xxx" },
295
+ });
296
+
297
+ // Nested relationships in a single request
298
+ const detail = await client.graphql.query({
299
+ query: `query ($id: ID!) {
300
+ store(id: $id) {
301
+ id name
302
+ onetimeProducts { id name status prices }
303
+ subscriptionProducts { id name billingPeriod status }
304
+ }
305
+ }`,
306
+ variables: { id: "STO_xxx" },
266
307
  });
267
308
  ```
268
309
 
269
- ### Stores Create, Update, Delete
310
+ See [GraphQL Guide](docs/graphql-guide.md) for filters, analytics queries, delivery logs, and more.
311
+
312
+ ## Programmatic Store & Product Management
313
+
314
+ > Most merchants manage stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The following APIs are for merchants who need programmatic automation.
315
+
316
+ ### Stores
270
317
 
271
318
  ```typescript
272
319
  // Create a store
@@ -298,20 +345,20 @@ const { store: updated } = await client.stores.update({
298
345
  const { store: deleted } = await client.stores.delete({ id: store.id });
299
346
  ```
300
347
 
301
- ### Onetime Products — Create, Update, Publish
348
+ ### Products
302
349
 
303
350
  ```typescript
304
- import { TaxCategory, ProductVersionStatus } from "@waffo/pancake-ts";
351
+ import { TaxCategory, BillingPeriod, ProductVersionStatus } from "@waffo/pancake-ts";
305
352
 
306
- // Create with multi-currency pricing
353
+ // One-time product with multi-currency pricing
307
354
  const { product } = await client.onetimeProducts.create({
308
- storeId: "store_xxx",
355
+ storeId: "STO_xxx",
309
356
  name: "E-Book: TypeScript Handbook",
310
357
  description: "Complete TypeScript guide for developers",
311
358
  prices: {
312
- USD: { amount: 2900, taxCategory: TaxCategory.DigitalGoods },
313
- EUR: { amount: 2700, taxCategory: TaxCategory.DigitalGoods },
314
- JPY: { amount: 4500, taxCategory: TaxCategory.DigitalGoods },
359
+ USD: { amount: "29.00", taxCategory: TaxCategory.DigitalGoods },
360
+ EUR: { amount: "27.00", taxCategory: TaxCategory.DigitalGoods },
361
+ JPY: { amount: "4500", taxCategory: TaxCategory.DigitalGoods },
315
362
  },
316
363
  media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
317
364
  metadata: { sku: "ebook-ts-001" },
@@ -321,7 +368,7 @@ const { product } = await client.onetimeProducts.create({
321
368
  await client.onetimeProducts.update({
322
369
  id: product.id,
323
370
  name: "E-Book: TypeScript Handbook v2",
324
- prices: { USD: { amount: 3900, taxCategory: "digital_goods" } },
371
+ prices: { USD: { amount: "39.00", taxCategory: "digital_goods" } },
325
372
  });
326
373
 
327
374
  // Publish test version → production
@@ -329,40 +376,32 @@ await client.onetimeProducts.publish({ id: product.id });
329
376
 
330
377
  // Deactivate
331
378
  await client.onetimeProducts.updateStatus({ id: product.id, status: ProductVersionStatus.Inactive });
332
- ```
333
-
334
- ### Subscription Products — Create with Billing Period
335
-
336
- ```typescript
337
- import { BillingPeriod, TaxCategory } from "@waffo/pancake-ts";
338
379
 
339
- const { product } = await client.subscriptionProducts.create({
340
- storeId: "store_xxx",
380
+ // Subscription product
381
+ const { product: sub } = await client.subscriptionProducts.create({
382
+ storeId: "STO_xxx",
341
383
  name: "Pro Plan",
342
384
  billingPeriod: BillingPeriod.Monthly,
343
- prices: { USD: { amount: 999, taxCategory: TaxCategory.SaaS } },
344
- description: "Unlimited access to all features",
385
+ prices: { USD: { amount: "9.99", taxCategory: TaxCategory.SaaS } },
345
386
  });
346
-
347
- // Same update/publish/updateStatus pattern as onetime products
348
- await client.subscriptionProducts.publish({ id: product.id });
387
+ await client.subscriptionProducts.publish({ id: sub.id });
349
388
  ```
350
389
 
351
- ### Subscription Product Groups — Shared Trial & Plan Switching
390
+ ### Subscription Product Groups
352
391
 
353
392
  ```typescript
354
393
  // Create a group linking related subscription tiers
355
394
  const { group } = await client.subscriptionProductGroups.create({
356
- storeId: "store_xxx",
395
+ storeId: "STO_xxx",
357
396
  name: "Pro Plans",
358
397
  rules: { sharedTrial: true },
359
- productIds: ["prod_aaa", "prod_bbb"],
398
+ productIds: ["PROD_aaa", "PROD_bbb"],
360
399
  });
361
400
 
362
401
  // Update members (full replacement, not merge)
363
402
  await client.subscriptionProductGroups.update({
364
403
  id: group.id,
365
- productIds: ["prod_aaa", "prod_bbb", "prod_ccc"],
404
+ productIds: ["PROD_aaa", "PROD_bbb", "PROD_ccc"],
366
405
  });
367
406
 
368
407
  // Publish / delete
@@ -370,161 +409,15 @@ await client.subscriptionProductGroups.publish({ id: group.id });
370
409
  await client.subscriptionProductGroups.delete({ id: group.id });
371
410
  ```
372
411
 
373
- ### Orders — Cancel a Subscription
412
+ ### Orders
374
413
 
375
414
  ```typescript
376
415
  const { orderId, status } = await client.orders.cancelSubscription({
377
- orderId: "order_xxx",
416
+ orderId: "ORD_xxx",
378
417
  });
379
418
  // status: "canceled" (was pending) or "canceling" (was active, PSP notified)
380
419
  ```
381
420
 
382
- ### Checkout — Create a Session
383
-
384
- ```typescript
385
- import { CheckoutSessionProductType } from "@waffo/pancake-ts";
386
-
387
- // One-time product checkout
388
- const session = await client.checkout.createSession({
389
- storeId: "store_xxx",
390
- productId: "prod_xxx",
391
- productType: CheckoutSessionProductType.Onetime,
392
- currency: "USD",
393
- buyerEmail: "customer@example.com",
394
- successUrl: "https://example.com/thank-you",
395
- });
396
- // => redirect buyer to session.checkoutUrl
397
-
398
- // Subscription with trial and billing detail
399
- const subSession = await client.checkout.createSession({
400
- storeId: "store_xxx",
401
- productId: "prod_yyy",
402
- productType: CheckoutSessionProductType.Subscription,
403
- currency: "USD",
404
- withTrial: true,
405
- billingDetail: { country: "US", isBusiness: false, state: "CA", postcode: "94105" },
406
- });
407
- ```
408
-
409
- ### GraphQL — Typed Queries
410
-
411
- ```typescript
412
- // Simple query
413
- interface StoresQuery {
414
- stores: Array<{ id: string; name: string; status: string }>;
415
- }
416
- const result = await client.graphql.query<StoresQuery>({
417
- query: `query { stores { id name status } }`,
418
- });
419
-
420
- // Query with variables
421
- const product = await client.graphql.query({
422
- query: `query ($id: ID!) { onetimeProduct(id: $id) { id name prices } }`,
423
- variables: { id: "prod_xxx" },
424
- });
425
-
426
- // Nested relationships in a single request
427
- const detail = await client.graphql.query({
428
- query: `query ($id: ID!) {
429
- store(id: $id) {
430
- id name
431
- onetimeProducts { id name status prices }
432
- subscriptionProducts { id name billingPeriod status }
433
- }
434
- }`,
435
- variables: { id: "store_xxx" },
436
- });
437
- ```
438
-
439
- See [GraphQL Guide](docs/graphql-guide.md) for introspection, filters, pagination, and more examples.
440
-
441
- ## Webhook Verification
442
-
443
- Two ways to verify webhooks: the **standalone function** `verifyWebhook()` with built-in public keys, or the **client instance method** `client.webhooks.verify()` which uses the configured `webhookPublicKey`.
444
-
445
- ### Option A — Standalone Function (built-in keys)
446
-
447
- ```typescript
448
- import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
449
-
450
- // Express (IMPORTANT: use raw body — parsed JSON breaks signature verification)
451
- app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
452
- try {
453
- const event = verifyWebhook(
454
- req.body.toString("utf-8"),
455
- req.headers["x-waffo-signature"] as string,
456
- );
457
-
458
- // Respond immediately, process asynchronously
459
- res.status(200).send("OK");
460
-
461
- // Use event.id for idempotent deduplication
462
- switch (event.eventType) {
463
- case WebhookEventType.OrderCompleted:
464
- console.log(`Order ${event.data.orderId} completed`);
465
- break;
466
- case WebhookEventType.SubscriptionActivated:
467
- console.log(`Subscription activated for ${event.data.buyerEmail}`);
468
- break;
469
- case WebhookEventType.SubscriptionCanceled:
470
- console.log(`Subscription canceled: ${event.data.orderId}`);
471
- break;
472
- case WebhookEventType.RefundSucceeded:
473
- console.log(`Refund ${event.data.amount} ${event.data.currency}`);
474
- break;
475
- }
476
- } catch {
477
- res.status(401).send("Invalid signature");
478
- }
479
- });
480
-
481
- // Next.js App Router
482
- export async function POST(request: Request) {
483
- const body = await request.text();
484
- const sig = request.headers.get("x-waffo-signature");
485
- try {
486
- const event = verifyWebhook(body, sig);
487
- // handle event ...
488
- return new Response("OK");
489
- } catch {
490
- return new Response("Invalid signature", { status: 401 });
491
- }
492
- }
493
-
494
- // Options: specify environment, disable/customize replay protection
495
- const event = verifyWebhook(body, sig, { environment: "prod" });
496
- const event = verifyWebhook(body, sig, { toleranceMs: 0 }); // disable replay check
497
- ```
498
-
499
- ### Option B — Client Instance Method (multi-level key resolution)
500
-
501
- `client.webhooks.verify()` uses the [multi-level fallback chain](#webhook-public-key-resolution) automatically: config keys → env vars → built-in keys.
502
-
503
- ```typescript
504
- // Per-environment keys via config
505
- const client = new WaffoPancake({
506
- merchantId: process.env.WAFFO_MERCHANT_ID!,
507
- privateKey: process.env.WAFFO_PRIVATE_KEY!,
508
- webhookPublicKey: {
509
- test: process.env.WAFFO_TEST_PUB_KEY!,
510
- prod: process.env.WAFFO_PROD_PUB_KEY!,
511
- },
512
- });
513
- const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
514
-
515
- // Or rely on env vars (WAFFO_WEBHOOK_TEST_PUBLIC_KEY / WAFFO_WEBHOOK_PROD_PUBLIC_KEY)
516
- const client2 = new WaffoPancake({
517
- merchantId: process.env.WAFFO_MERCHANT_ID!,
518
- privateKey: process.env.WAFFO_PRIVATE_KEY!,
519
- });
520
- const event2 = client2.webhooks.verify(rawBody, sig); // auto-detect environment
521
-
522
- // Per-call override (highest priority, skips all resolution)
523
- const event3 = client.webhooks.verify(rawBody, sig, { publicKey: oneOffKey });
524
- ```
525
-
526
- See [Webhook Guide](docs/webhook-guide.md) for event types, signature algorithm, public key resolution, and best practices.
527
-
528
421
  ## Error Handling
529
422
 
530
423
  API errors throw `WaffoPancakeError` with the HTTP status code and a call-stack-ordered errors array.
@@ -543,25 +436,46 @@ try {
543
436
  }
544
437
  ```
545
438
 
439
+ ## Resources
440
+
441
+ | Namespace | Methods | Description |
442
+ |-----------|---------|-------------|
443
+ | `client.checkout.authenticated` | `create()` | Authenticated checkout (recommended) |
444
+ | `client.checkout.anonymous` | `create()` | Anonymous checkout |
445
+ | `client.checkout` | `createSession()` | Low-level checkout session |
446
+ | `client.buyer(token)` | `cancelSubscription()` `cancelOnetimeOrder()` `reactivateSubscription()` `createRefundTicket()` `resubmitRefundTicket()` | Buyer self-service |
447
+ | `client.buyer(token).graphql` | `query<T>()` | Buyer-scoped GraphQL queries |
448
+ | `client.webhooks` | `verify<T>()` | Webhook signature verification |
449
+ | `client.graphql` | `query<T>()` | Merchant GraphQL queries |
450
+ | `client.auth` | `issueSessionToken()` | Issue a buyer session token (JWT) |
451
+ | `client.stores` | `create()` `update()` `delete()` | Store management |
452
+ | `client.storeMerchants` | `add()` `remove()` `updateRole()` | Store members (coming soon) |
453
+ | `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time products |
454
+ | `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription products |
455
+ | `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups |
456
+ | `client.orders` | `cancelSubscription()` | Order management |
457
+
458
+ ## Documentation
459
+
460
+ | Document | Content |
461
+ |----------|---------|
462
+ | [API Reference](docs/api-reference.md) | Complete method reference — parameters, return types, `BillingDetail` fields |
463
+ | [GraphQL Guide](docs/graphql-guide.md) | Queries, filters, analytics, introspection, delivery logs |
464
+ | [Webhook Guide](docs/webhook-guide.md) | Signature verification, event types, key resolution, retry mechanism |
465
+ | [Changelog](CHANGELOG.md) | Version history and migration guides |
466
+
546
467
  ## Exports
547
468
 
548
- ### Classes
469
+ ### Classes & Functions
549
470
 
550
471
  | Export | Description |
551
472
  |--------|-------------|
552
473
  | `WaffoPancake` | SDK client with auto-signed requests |
553
474
  | `WaffoPancakeError` | API error with status and call-stack errors |
554
-
555
- ### Functions
556
-
557
- | Export | Description |
558
- |--------|-------------|
559
- | `verifyWebhook` | RSA-SHA256 webhook signature verification |
475
+ | `verifyWebhook` | Standalone webhook signature verification |
560
476
 
561
477
  ### Enums
562
478
 
563
- Runtime-accessible values. Both `Enum.Value` and string literal syntax are supported.
564
-
565
479
  | Export | Values |
566
480
  |--------|--------|
567
481
  | `Environment` | `Test`, `Prod` |
@@ -571,18 +485,18 @@ Runtime-accessible values. Both `Enum.Value` and string literal syntax are suppo
571
485
  | `EntityStatus` | `Active`, `Inactive`, `Suspended` |
572
486
  | `StoreRole` | `Owner`, `Admin`, `Member` |
573
487
  | `OnetimeOrderStatus` | `Pending`, `Completed`, `Canceled` |
574
- | `SubscriptionOrderStatus` | `Pending`, `Active`, `Canceling`, `Canceled`, `PastDue`, `Expired` |
488
+ | `SubscriptionOrderStatus` | `Pending`, `Active`, `Canceling`, `PastDue`, `Closed`, `Canceled`, `Expired` |
575
489
  | `PaymentStatus` | `Pending`, `Succeeded`, `Failed`, `Canceled` |
576
490
  | `RefundTicketStatus` | `Pending`, `Approved`, `Rejected`, `Processing`, `Succeeded`, `Failed` |
577
491
  | `RefundStatus` | `Succeeded`, `Failed` |
578
492
  | `MediaType` | `Image`, `Video` |
579
493
  | `CheckoutSessionProductType` | `Onetime`, `Subscription` |
580
- | `ErrorLayer` | `Gateway`, `User`, `Store`, `Product`, `Order`, `GraphQL`, `Resource`, `Email` |
494
+ | `ErrorLayer` | `Gateway`, `User`, `Store`, `Product`, `Order`, `Ticket`, `GraphQL`, `Resource`, `Email` |
581
495
  | `WebhookEventType` | `OrderCompleted`, `SubscriptionActivated`, `SubscriptionPaymentSucceeded`, `SubscriptionCanceling`, `SubscriptionUncanceled`, `SubscriptionUpdated`, `SubscriptionCanceled`, `SubscriptionPastDue`, `RefundSucceeded`, `RefundFailed` |
582
496
 
583
497
  ### Types
584
498
 
585
- Key types: `WaffoPancakeConfig`, `WebhookPublicKeys`, `VerifyWebhookOptions`, `WebhookEvent<T>`, `Store`, `OnetimeProductDetail`, `SubscriptionProductDetail`, `CheckoutSessionResult`, `GraphQLResponse<T>`, and 30+ more. See [API Reference — Types](docs/api-reference.md#types) for the full list.
499
+ Key types: `WaffoPancakeConfig`, `AuthenticatedCheckoutParams`, `AuthenticatedCheckoutResult`, `AnonymousCheckoutParams`, `CheckoutSessionResult`, `Store`, `OnetimeProductDetail`, `SubscriptionProductDetail`, `WebhookEvent<T>`, `GraphQLResponse<T>`, and 30+ more. See [API Reference](docs/api-reference.md#types) for the full list.
586
500
 
587
501
  ## Development
588
502
 
@@ -591,7 +505,7 @@ npm run lint # ESLint 9 (TypeScript ESLint + import order + JSDoc)
591
505
  npm run test # Vitest
592
506
  npm run test:watch # Vitest in watch mode
593
507
  npm run test:coverage # Vitest with v8 coverage
594
- npm run build # TypeScript compilation to dist/
508
+ npm run build # tsup ESM + CJS + DTS
595
509
  ```
596
510
 
597
511
  ## Project Structure
@@ -600,7 +514,8 @@ npm run build # TypeScript compilation to dist/
600
514
  src/
601
515
  ├── index.ts # Unified export entry
602
516
  ├── client.ts # WaffoPancake main class
603
- ├── http-client.ts # HTTP client (auto-signing + idempotency)
517
+ ├── http-client.ts # HTTP client (API Key, auto-signing + idempotency)
518
+ ├── buyer-http-client.ts # HTTP client (Bearer token, buyer self-service)
604
519
  ├── signing.ts # RSA-SHA256 request signing
605
520
  ├── errors.ts # WaffoPancakeError
606
521
  ├── webhooks.ts # Webhook verification (embedded keys)
@@ -613,13 +528,16 @@ src/
613
528
  ├── onetime-products.ts
614
529
  ├── subscription-products.ts
615
530
  ├── subscription-product-groups.ts
531
+ ├── buyer.ts
616
532
  ├── orders.ts
617
533
  ├── checkout.ts
534
+ ├── checkout-anonymous.ts
535
+ ├── checkout-authenticated.ts
618
536
  ├── graphql.ts
619
537
  └── webhooks.ts
620
538
  docs/
621
539
  ├── api-reference.md # Complete API reference
622
- ├── graphql-guide.md # GraphQL usage guide
540
+ ├── graphql-guide.md # GraphQL queries & analytics
623
541
  └── webhook-guide.md # Webhook verification guide
624
542
  ```
625
543