# @waffo/pancake-ts

> TypeScript SDK for Waffo Pancake MoR platform — RSA-SHA256 signing, zero runtime dependencies

Latest version **0.24.0** (published 2026-09-21) · MIT license · 0 weekly downloads

## Install

```sh
npm install @waffo/pancake-ts
pnpm add @waffo/pancake-ts
yarn add @waffo/pancake-ts
bun add @waffo/pancake-ts
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.24.0 |
| Published | 2026-09-21 |
| First published | 2026-03-10 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=20.0.0 |
| Dependencies | 0 |
| Unpacked size | 939.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 1 |
| Maintainers | zephyr-waffo, mohuiling, waffo-js, zhongyuan.zhao, hill-waffo, tangjiaj, fronzenvista |

## Links

- npm: https://www.npmjs.com/package/@waffo/pancake-ts
- Repository: https://github.com/waffo-com/waffo-pancake-sdk-ts
- Homepage: https://github.com/waffo-com/waffo-pancake-sdk-ts#readme
- Issues: https://github.com/waffo-com/waffo-pancake-sdk-ts/issues
- npm.io page: https://npm.io/package/@waffo/pancake-ts

## Recent versions

- 0.24.0 (latest) — 2026-09-21
- 0.23.0 — 2026-09-20
- 0.22.0 — 2026-09-19
- 0.21.0 — 2026-09-10
- 0.20.0 — 2026-09-06
- 0.19.1 — 2026-08-21
- 0.19.0 — 2026-08-18
- 0.18.0 — 2026-08-08
- 0.17.0 — 2026-08-04
- 0.16.1 — 2026-07-30
- 0.16.0 — 2026-07-28
- 0.15.0 — 2026-07-27
- 0.14.0 — 2026-07-18
- 0.13.0 — 2026-07-17
- 0.12.0 — 2026-07-14
- … 26 more at https://npm.io/package/@waffo/pancake-ts/versions

## README

# @waffo/pancake-ts

TypeScript SDK for the Waffo Pancake Merchant of Record (MoR) payment platform.

- Zero runtime dependencies, ESM + CJS, Node >= 20
- Automatic RSA-SHA256 request signing; opt-in idempotency keys per call
- Full TypeScript type definitions (15 enums, 40+ interfaces)
- Webhook verification with embedded public keys (test/prod)

## Installation

```bash
npm install @waffo/pancake-ts
```

## Quick Start

> Most merchants create stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The SDK is primarily used for **checkout integration** — redirecting customers from your site to the Waffo checkout page.

```typescript
import { WaffoPancake } from "@waffo/pancake-ts";

// Merchant ID and API Key are available in Dashboard > Settings > Developers
const client = new WaffoPancake({
  merchantId: process.env.WAFFO_MERCHANT_ID!, // MER_{base62} format
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
});

// Create a checkout session — one call handles token + session + URL
const result = await client.checkout.authenticated.create({
  productId: "PROD_xxx", // from Dashboard > Products
  currency: "USD",
  buyerIdentity: req.user.email, // your user's identity
});

// Redirect customer to the checkout page (opens in new tab)
res.json({ checkoutUrl: result.checkoutUrl });
// => checkoutUrl includes #token=... (form pre-filled)
```

## Configuration

| Parameter          | Type                         | Required              | Description                                                                                               |
| ------------------ | ---------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------- |
| `merchantId`       | `string`                     | Yes                   | Merchant ID in `MER_{base62}` format                                                                      |
| `privateKey`       | `string`                     | Yes                   | RSA private key in PEM format (auto-normalized, see [docs](docs/api-reference.md))                        |
| `baseUrl`          | `string`                     | No                    | API base URL override                                                                                     |
| `environment`      | `"test" \| "prod"`           | For customer sessions | Sent as `X-Environment`. No default — override per session with `client.customer(token, { environment })` |
| `fetch`            | `typeof fetch`               | No                    | Custom fetch implementation                                                                               |
| `webhookPublicKey` | `string \| { test?, prod? }` | No                    | Custom webhook public key(s)                                                                              |

The SDK auto-normalizes key formats: standard PEM, PKCS#1, literal `\n` from env vars, raw base64, and Windows line endings are all accepted.

## Checkout Integration

Waffo supports two checkout modes based on whether the merchant knows the customer's identity:

- **Merchants with their own sites** know who the customer is — they have user accounts, login systems, or collect customer info before checkout. The merchant provides the customer's identity upfront, and the checkout form arrives pre-filled.
- **Template stores and shared links** have no prior customer context — the customer arrives directly at the checkout page and fills in their own details.

| Mode              | Method                            | Customer Identity | Form State | Use Case                                 |
| ----------------- | --------------------------------- | ----------------- | ---------- | ---------------------------------------- |
| **Authenticated** | `checkout.authenticated.create()` | Merchant provides | Pre-filled | Merchant sites with user accounts        |
| **Anonymous**     | `checkout.anonymous.create()`     | Not provided      | Empty      | Template stores, one-time purchase links |

Changing the plan of an existing subscription is a separate pair of methods — see [Plan Change Links](#plan-change-links).

> **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 customer changes the email on the checkout form, the order is still tied to the identity you specified. In anonymous mode, the customer 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).
>
> |                   | Authenticated                                                           | Anonymous                                          |
> | ----------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |
> | **Identity**      | Merchant-provided, stable across orders                                 | Self-reported email, may vary                      |
> | **Form**          | Pre-filled from merchant-provided identity                              | Empty, customer fills manually                     |
> | **Post-purchase** | Full self-service (see [Customer Self-Service](#customer-self-service)) | Create orders only — no post-purchase self-service |
> | **Session**       | 5-minute TTL, auto-refreshes                                            | 1-minute, single-use                               |

Both modes support **dynamic pricing** and **trial control** at checkout time:

- `priceSnapshot` — override the product's stored price with a custom amount (e.g., coupon, volume discount); for subscription products this replaces the regular period price only
- `withTrial` — explicitly enable or disable the trial period for subscriptions (`true` = force trial, `false` = skip trial, omit = use default rules)

### Authenticated Checkout (Recommended)

The merchant provides customer 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.

`buyerIdentity` is for order attribution and trial tracking only — it is not rendered on the checkout page. To pre-fill the email field on the checkout form, pass `buyerEmail` explicitly.

```typescript
// Basic — customer identity only (checkout page email field stays empty)
const result = await client.checkout.authenticated.create({
  productId: "PROD_xxx",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
});

// With dynamic pricing — override stored price (e.g., coupon, volume discount)
const result = await client.checkout.authenticated.create({
  productId: "PROD_xxx",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
  buyerEmail: "customer@example.com",
  priceSnapshot: { amount: "19.99", taxCategory: "digital_goods" },
});

// Subscription with trial control + billing detail pre-fill
const result = await client.checkout.authenticated.create({
  productId: "PROD_xxx",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
  buyerEmail: "customer@example.com",
  withTrial: true, // force enable trial (false = skip, omit = default rules)
  billingDetail: { country: "US", isBusiness: false },
  orderMerchantExternalId: "ORDER-2026-00891", // optional, see Business-Side Identifiers below
});

// result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}"
window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
```

The token is passed via the URL fragment (after `#`), which is never sent to the server and never appears in the `Referer` header.

### Anonymous Checkout

No customer identity required — the customer fills in billing details manually on the checkout page.

```typescript
const result = await client.checkout.anonymous.create({
  productId: "PROD_xxx",
  currency: "USD",
});

// Also supports priceSnapshot, withTrial, and orderMerchantExternalId
const result = await client.checkout.anonymous.create({
  productId: "PROD_xxx",
  currency: "USD",
  priceSnapshot: { amount: "4.99", taxCategory: "saas" },
  withTrial: false, // skip trial for this session
  orderMerchantExternalId: "ORDER-2026-00891", // optional, API Key auth only
  language: "pt-BR", // optional, sets the default checkout language (IETF BCP 47)
  includePaymentMethods: ["card", "applepay"], // optional whitelist; or excludePaymentMethods to drop specific ones
});

window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
```

### Plan Change Links

Switching an existing subscription to another plan uses the same endpoint in a different mode, so the SDK gives it its own methods with `originOrderId` required — the platform rejects the plan change fields whenever they appear without it, and a required field makes that unrepresentable.

```typescript
import { ChangeTiming } from "@waffo/pancake-ts";

// API Key entry point — you send the customer to the returned URL yourself
const session = await client.checkout.createPlanChangeSession({
  originOrderId: "ORD_xxx", // the subscription being changed (required)
  productId: "PROD_target_plan", // the plan to switch to
  currency: "USD",
  changeTiming: ChangeTiming.Immediate, // omit to let the platform derive it
  changeCreditAmount: "8.00", // "credit this much" — or changeAmount, never both
});
// session.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/change/{sessionId}"

// Authenticated entry point — same split as authenticated checkout, the token is
// appended so the customer lands on the confirmation page already signed in
const result = await client.checkout.authenticated.createPlanChange({
  originOrderId: "ORD_xxx",
  productId: "PROD_target_plan",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
  changeTiming: ChangeTiming.NextPeriod,
});
// result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/change/{sessionId}#token={JWT}"
```

- **`changeAmount` vs `changeCreditAmount`** — two ways to price the same change: `changeAmount` sets what you charge for this period, `changeCreditAmount` sets how much you credit against it. Same unit and tax basis, opposite meaning, so they are mutually exclusive and sending both is rejected with a 400.
- **Merchant credentials only** — `changeAmount`, `changeCreditAmount` and `withTrial` are honored because these calls are signed with your API Key. A customer-session credential calling the endpoint directly has them silently dropped.
- **Anonymous has no plan change** — a Store Slug session has no subscription to attribute the change to (the platform answers 403), so `checkout.anonymous` carries no plan change method at all.
- **Customer self-service** — to let customers start a change themselves, switch on `selfServicePlanChange` on the product group (see [Subscription Product Groups](#subscription-product-groups)) and call `customer.createPlanChangeSession()` on their session (see [Customer Self-Service](#customer-self-service)). That path is the only one the switch gates; merchant-issued links ignore it.

### Opening the Checkout Page

**We recommend opening the checkout page in a new tab** rather than navigating in the current page:

- Customers can return to your site immediately after payment or if they close the checkout tab
- Merchant page state (cart, forms, scroll position) is preserved
- Payment flow is decoupled from the browsing experience, reducing checkout abandonment

```typescript
// Recommended: open in a new tab
window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");

// Or via an <a> tag
// <a href={checkoutUrl} target="_blank" rel="noopener noreferrer">Proceed to Checkout</a>
```

> **Not recommended:** `window.location.href = result.checkoutUrl` replaces the current page, preventing customers from returning to your site without browser back navigation.

See [API Reference — Checkout](docs/api-reference.md#checkout) for full parameter tables and `BillingDetail` field requirements.

## Webhook Verification

After a customer completes payment, Waffo sends webhook events to your server with rich data including order details, amounts, product info, and event-specific fields (payment, subscription, or refund). The SDK provides two ways to verify signatures:

### Standalone Function (built-in keys)

```typescript
import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";

// Express (IMPORTANT: use raw body — parsed JSON breaks signature verification)
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = verifyWebhook(req.body.toString("utf-8"), req.headers["x-waffo-signature"] as string);

    // Respond immediately, process asynchronously
    res.status(200).send("OK");

    switch (event.eventType) {
      case WebhookEventType.OrderCompleted:
        // Rich data: order, amount, product, payment fields
        console.log(`Order ${event.data.orderId} completed — ${event.data.total} ${event.data.currency}`);
        console.log(`Product: ${event.data.productName}, Customer: ${event.data.buyerEmail}`);
        if (event.data.orderMetadata) console.log("Metadata:", event.data.orderMetadata);
        break;
      case WebhookEventType.SubscriptionActivated:
        console.log(`Subscription activated for ${event.data.buyerEmail}`);
        console.log(`Period: ${event.data.billingPeriod}, ends ${event.data.currentPeriodEnd}`);
        break;
      case WebhookEventType.RefundSucceeded:
        console.log(`Refund succeeded: ${event.data.refundReason}`);
        // refund.* events carry both business identifiers (see Business-Side Identifiers section)
        await ledger.closeRefundTicket(event.data.refundTicketMerchantExternalId, event.data.orderMerchantExternalId);
        break;
    }
  } catch {
    res.status(401).send("Invalid signature");
  }
});

// Next.js App Router
export async function POST(request: Request) {
  const body = await request.text();
  const sig = request.headers.get("x-waffo-signature");
  try {
    const event = verifyWebhook(body, sig);
    return new Response("OK");
  } catch {
    return new Response("Invalid signature", { status: 401 });
  }
}
```

### Client Instance Method (multi-level key resolution)

```typescript
const client = new WaffoPancake({
  merchantId: "MER_xxx",
  privateKey: "...",
  webhookPublicKey: {
    test: process.env.WAFFO_TEST_PUB_KEY!,
    prod: process.env.WAFFO_PROD_PUB_KEY!,
  },
});
const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
```

See [Webhook Guide](docs/webhook-guide.md) for event types, `WebhookEventData` field reference, dual-environment key architecture, key resolution chain, retry mechanism, and best practices.

## Customer Self-Service

Beyond checkout, you can let customers manage their own orders and subscriptions — for example, embedding a "Cancel Subscription" or "Request Refund" button in your site.

Issue a session token, then use `client.customer(token, options?)` to get a session with self-service methods:

```typescript
// Your backend — issue a session token for the customer
const { token } = await client.auth.issueSessionToken({
  storeId: "STO_xxx",
  buyerIdentity: req.user.email,
});

// Create a customer session — environment is required here (or on the client config),
// because a session token carries none of its own
const customer = client.customer(token, { environment: "test" });

// Cancel a subscription
const { orderId, status } = await customer.cancelSubscription({ orderId: "ORD_xxx" });
// status: "canceling" (active) or "canceled" (pending)

// Reactivate a canceled subscription
await customer.reactivateSubscription({ orderId: "ORD_xxx" });

// Cancel a one-time order (while payment is pending)
await customer.cancelOnetimeOrder({ orderId: "ORD_yyy" });

// Submit a refund request
const { ticket } = await customer.createRefundTicket({
  paymentId: "PAY_xxx",
  reason: "Product not as described",
  requestedAmount: { amount: "29.00", currency: "USD" },
  refundTicketMerchantExternalId: "REF-2026-00012", // optional, see Business-Side Identifiers below
});

// Resubmit a rejected refund ticket
await customer.resubmitRefundTicket({
  ticketId: "TKT_xxx",
  paymentId: "PAY_xxx",
  reason: "Updated reason with more detail",
  requestedAmount: { amount: "29.00", currency: "USD" },
});

// Let the customer switch their own subscription to another plan in the same group
const session = await customer.createPlanChangeSession({
  originOrderId: "ORD_xxx",
  productId: "PROD_target_plan",
  currency: "USD",
});
// Send them to session.checkoutUrl to confirm

// Query the customer's own orders via GraphQL
const result = await customer.graphql.query({
  query: `query { orders { id status createdAt } }`,
});
```

The token is scoped to the specified store and customer identity — customers can only access their own data. Token TTL is 5 minutes and auto-refreshes on each API call.

> **Note**: This uses the same `buyerIdentity` as `checkout.authenticated.create()`. Orders placed via authenticated checkout are automatically tied to this identity, so the customer can manage them later with a token issued here.

> **Customer-initiated plan change has three preconditions**, all enforced by the platform with a 403: the subscription belongs to this customer, the target plan is in the **same product group** as the current one, and that group's **`selfServicePlanChange`** is on (see [Subscription Product Groups](#subscription-product-groups)). A merchant issuing the link with the API Key is subject to none of them. The merchant-only pricing fields (`changeAmount`, `changeCreditAmount`, `withTrial`, `priceSnapshot`, …) are not part of the customer params — the platform drops them on this path without reporting it.

> **Customer session writes follow the same idempotency rule as every other method**: no key is sent unless you pass one, so a write retried after a timeout executes twice. See [Idempotency](#idempotency).

## Business-Side Identifiers

Attach your own internal references to a checkout or a refund ticket so cross-system reconciliation does not require Waffo IDs. Two flat keys, both optional (max 128 chars):

| Field                            | Attach at                                   | Inherited by                                               |
| -------------------------------- | ------------------------------------------- | ---------------------------------------------------------- |
| `orderMerchantExternalId`        | `checkout.{authenticated,anonymous}.create` | `Order`, `Payment` (incl. subscription renewals), `Refund` |
| `refundTicketMerchantExternalId` | `customer.createRefundTicket`               | `RefundTicket`, `Refund`                                   |

The same field name appears at every layer it surfaces: request body, response entity, webhook payload (`data.orderMerchantExternalId` / `data.refundTicketMerchantExternalId`), and every GraphQL type that carries the value. A `refund.*` webhook event carries **both** keys (order key inherited from the originating order). Query by either key via GraphQL filters — see [GraphQL Guide](docs/graphql-guide.md).

## GraphQL — Typed Queries

```typescript
// Simple query
interface StoresQuery {
  stores: Array<{ id: string; name: string; status: string }>;
}
const result = await client.graphql.query<StoresQuery>({
  query: `query { stores { id name status } }`,
});

// Query with variables
const product = await client.graphql.query({
  query: `query ($id: ID!) { onetimeProduct(id: $id) { id name prices } }`,
  variables: { id: "PROD_xxx" },
});

// Nested relationships in a single request
const detail = await client.graphql.query({
  query: `query ($id: ID!) {
    store(id: $id) {
      id name
      onetimeProducts { id name status prices }
      subscriptionProducts { id name billingPeriod status }
    }
  }`,
  variables: { id: "STO_xxx" },
});

// Look up by your business-side identifier (see Business-Side Identifiers above)
const byRef = await client.graphql.query({
  query: `query ($ref: String!) {
    payments(filter: { orderMerchantExternalId: { eq: $ref } }) {
      id orderId status orderMerchantExternalId
    }
  }`,
  variables: { ref: "ORDER-2026-00891" },
});
```

See [GraphQL Guide](docs/graphql-guide.md) for filters, analytics queries, delivery logs, and more.

## Warnings (Migration Notices)

Every successful REST action and GraphQL query may carry a `warnings` array alongside the data. Warnings describe non-fatal advisories the server wants you to act on — typically deprecated parameters, fields scheduled for removal, or new APIs you should switch to. Each `Notice` has `message` (human-readable), `layer` (which service produced it), and `aiHint` (a structured migration instruction aimed at LLM consumers).

```typescript
// REST action — warnings spread onto the result alongside the typed payload
const { store, warnings } = await client.stores.update({
  id: "STO_xxx",
  webhookSettings: { ... },  // deprecated input
});
if (warnings) {
  for (const w of warnings) {
    console.warn(`[${w.layer}] ${w.message}`, w.aiHint);
    // e.g. layer=store, aiHint="Switch to client.webhooks.add / update / remove"
  }
}

// GraphQL — warnings sit on the envelope alongside data and errors
const result = await client.graphql.query<StoresQuery>({
  query: `query { stores { id } }`,
});
result.warnings?.forEach(w => console.warn(w.message, w.aiHint));
```

**LLM/agent consumers**: always check `aiHint` on every warning — it is the canonical migration instruction (npm package, version, method name, endpoint path) the platform team intends for you to follow when the underlying API evolves.

## Programmatic Store & Product Management

> Most merchants manage stores and products in the [Dashboard](https://pancake.waffo.ai/dashboard). The following APIs are for merchants who need programmatic automation.

### Stores

```typescript
// Create a store
const { store } = await client.stores.create({ name: "My Store" });

// Update settings (notification, checkout theme).
// NOTE: webhook configuration moved to client.webhooks (see Webhooks section below).
// NOTE: writable here are all the merchant-facing `notify*` toggles (notifyNewOrders /
//       notifyNewSubscriptions / notifySubscription* / notifyChargeback /
//       notifyRefundSucceeded) plus the two buyer-facing reminders
//       emailUpcomingCharge (renewal) and emailTrialEnding (trial ending). Every
//       other consumer email toggle (emailOrderConfirmation, emailSubscription*,
//       emailTrialStarted, emailRefundSucceeded) is managed by the PANCAKE
//       platform and silently dropped if passed. Payout result emails are
//       always delivered and have no toggle.
// NOTE: both reminders are decided per store — every buyer of the store is covered
//       by the same switch, and passing notificationSettings: null puts them back on.
// NOTE: supportEmail and website are not writable here — they are set by
//       ownership verification (email code / domain) or KYB approval.
const { store: updated } = await client.stores.update({
  id: store.id,
  notificationSettings: {
    notifyNewOrders: true,
    notifyNewSubscriptions: false,
    // Stop reminding this store's buyers before a renewal is charged.
    emailUpcomingCharge: false,
    // Stop reminding this store's buyers that their paid trial is about to end.
    emailTrialEnding: false,
  },
});

// Soft-delete
const { store: deleted } = await client.stores.delete({ id: store.id });
```

### Webhooks

Manage webhook endpoints across HTTP, Feishu, Discord, Telegram, and Slack. Each store can have up to 20 webhooks across all channels.

```typescript
import { WebhookEventType } from "@waffo/pancake-ts";

// Add a standard HTTPS webhook (RSA-signed envelope)
const { webhook } = await client.webhooks.add({
  storeId: store.id,
  channel: "http",
  url: "https://example.com/webhooks/pancake",
  events: [WebhookEventType.OrderCompleted, WebhookEventType.RefundSucceeded],
  testMode: false,
});

// Add a Discord webhook (uses Discord embed format)
await client.webhooks.add({
  storeId: store.id,
  channel: "discord",
  url: "https://discord.com/api/webhooks/123/abc",
  events: [WebhookEventType.OrderCompleted],
  testMode: false,
});

// Add a Telegram webhook (chat_id stored in `secret`)
await client.webhooks.add({
  storeId: store.id,
  channel: "telegram",
  url: "https://api.telegram.org/bot123:ABC/sendMessage",
  events: [WebhookEventType.OrderCompleted],
  testMode: false,
  secret: "8737101383",
});

// Update events
await client.webhooks.update({
  id: webhook.id,
  events: [WebhookEventType.OrderCompleted, WebhookEventType.RefundSucceeded, WebhookEventType.SubscriptionCanceled],
});

// Hard-delete a webhook (delivery history retained for audit)
await client.webhooks.remove({ id: webhook.id });
```

> **Listing**: query the configured webhook list via GraphQL `Store.storeWebhooks` (filtered by environment automatically). The SDK does not expose a `list` method — `client.graphql.query` is the only read path, by design.

### Products

```typescript
import { TaxCategory, BillingPeriod, ProductVersionStatus } from "@waffo/pancake-ts";

// One-time product with multi-currency pricing
const { product } = await client.onetimeProducts.create({
  storeId: "STO_xxx",
  name: "E-Book: TypeScript Handbook",
  description: "Complete TypeScript guide for developers",
  prices: {
    USD: { amount: "29.00", taxCategory: TaxCategory.DigitalGoods },
    EUR: { amount: "27.00", taxCategory: TaxCategory.DigitalGoods },
    JPY: { amount: "4500", taxCategory: TaxCategory.DigitalGoods },
  },
  media: [{ type: "image", url: "https://example.com/cover.jpg", alt: "Book cover" }],
  metadata: { sku: "ebook-ts-001" },
});

// Update (creates a new immutable version; skips if unchanged)
await client.onetimeProducts.update({
  id: product.id,
  name: "E-Book: TypeScript Handbook v2",
  prices: { USD: { amount: "39.00", taxCategory: "digital_goods" } },
});

// Publish test version → production
await client.onetimeProducts.publish({ id: product.id });

// Deactivate
await client.onetimeProducts.updateStatus({ id: product.id, status: ProductVersionStatus.Inactive });

// Subscription product
const { product: sub } = await client.subscriptionProducts.create({
  storeId: "STO_xxx",
  name: "Pro Plan",
  billingPeriod: BillingPeriod.Monthly,
  prices: { USD: { amount: "9.99", taxCategory: TaxCategory.SaaS } },
});
await client.subscriptionProducts.publish({ id: sub.id });
```

### Subscription Product Groups

Both group switches are optional on create and update. `sharedTrial` shares the trial period across the group's products; `selfServicePlanChange` is the master switch for customers changing plans within the group from the customer portal — while it is off, a customer-credential plan change link is rejected with a 403 (merchant-issued links are unaffected).

```typescript
// Create a group linking related subscription tiers
const { group } = await client.subscriptionProductGroups.create({
  storeId: "STO_xxx",
  name: "Pro Plans",
  rules: { sharedTrial: true, selfServicePlanChange: true },
  productIds: ["PROD_aaa", "PROD_bbb"],
});

// Update members (full replacement, not merge)
await client.subscriptionProductGroups.update({
  id: group.id,
  productIds: ["PROD_aaa", "PROD_bbb", "PROD_ccc"],
});

// Rules are merged switch by switch — sharedTrial keeps its stored value here
const { group: updated } = await client.subscriptionProductGroups.update({
  id: group.id,
  rules: { selfServicePlanChange: true },
});
// updated.rules is always complete: { sharedTrial, selfServicePlanChange }

// Publish / delete
await client.subscriptionProductGroups.publish({ id: group.id });
await client.subscriptionProductGroups.delete({ id: group.id });
```

### Orders

```typescript
const { orderId, status } = await client.orders.cancelSubscription({
  orderId: "ORD_xxx",
});
// status: "canceled" (was pending) or "canceling" (was active, PSP notified)
```

### Content Safety

Scan a user's prompt for content-safety compliance before AIGC generation — continue only when `action` is `allow`. Stateless (prompt text is never stored); fails closed to `review` if the safety service is briefly unavailable.

```typescript
const verdict = await client.contentSafety.scanPrompt({ prompt: "a cat riding a bike" });
if (verdict.action !== "allow") {
  // do not generate — verdict.action is "review" or "block"
}
```

## Idempotency

**No idempotency key is sent unless you pass one.** The SDK does not derive keys — a write that times out and gets retried executes a second time unless you supplied a key the first time.

Pass one as the last argument of any write method:

```typescript
const { store } = await client.stores.create({ name: "My Store" }, { idempotencyKey: `MER_store-create-${requestId}` });
```

What the platform does with it:

| Situation                                | Result                                                |
| ---------------------------------------- | ----------------------------------------------------- |
| First request with this key              | Executes; the 2xx response is cached for **24 hours** |
| Same key again, original finished        | The cached response is returned, nothing re-executes  |
| Same key again, original still in flight | **409 Conflict**                                      |
| Original finished non-2xx                | The key is free; the same key can be retried          |
| No key at all                            | Nothing is deduplicated                               |

**Uniqueness is yours to guarantee.** At most 256 characters of letters, numbers, hyphens and underscores; a malformed key is rejected by the gateway with a 400. Use one key per logical operation — your own request/order id plus a prefix is the usual shape. Reusing a key across two different calls makes the second one replay the first one's response.

A key you pass to `checkout.authenticated.create()` or `.createPlanChange()` applies to the `create-session` call only — one key cannot address two endpoints, and re-issuing a session token is harmless.

GraphQL queries take no key: they are reads, and the cache would serve stale data.

## Error Handling

API errors throw `WaffoPancakeError` with the HTTP status code and a call-stack-ordered errors array.

```typescript
import { WaffoPancakeError } from "@waffo/pancake-ts";

try {
  await client.stores.create({ name: "" });
} catch (err) {
  if (err instanceof WaffoPancakeError) {
    console.log(err.status); // 400
    console.log(err.errors); // [{ message: "...", layer: "store" }, ...]
    // errors[0] = deepest layer, errors[n] = outermost layer
  }
}
```

## Resources

| Namespace                          | Methods                                                                                                                  | Description                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- |
| `client.checkout.authenticated`    | `create()`                                                                                                               | Authenticated checkout (recommended)    |
| `client.checkout.anonymous`        | `create()`                                                                                                               | Anonymous checkout                      |
| `client.checkout`                  | `createSession()`                                                                                                        | Low-level checkout session              |
| `client.customer(token, opts?)`    | `cancelSubscription()` `cancelOnetimeOrder()` `reactivateSubscription()` `createRefundTicket()` `resubmitRefundTicket()` | Customer self-service                   |
| `client.customer(token).graphql`   | `query<T>()`                                                                                                             | Customer-scoped GraphQL queries         |
| `client.webhooks`                  | `verify<T>()` `add()` `update()` `remove()`                                                                              | Webhook config + signature verification |
| `client.graphql`                   | `query<T>()`                                                                                                             | Merchant GraphQL queries                |
| `client.auth`                      | `issueSessionToken()`                                                                                                    | Issue a customer session token (JWT)    |
| `client.stores`                    | `create()` `update()` `delete()`                                                                                         | Store management                        |
| `client.storeMerchants`            | `add()` `remove()` `updateRole()`                                                                                        | Store members (coming soon)             |
| `client.onetimeProducts`           | `create()` `update()` `publish()` `updateStatus()`                                                                       | One-time products                       |
| `client.subscriptionProducts`      | `create()` `update()` `publish()` `updateStatus()`                                                                       | Subscription products                   |
| `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()`                                                                             | Product groups                          |
| `client.orders`                    | `cancelSubscription()`                                                                                                   | Order management                        |
| `client.contentSafety`             | `scanPrompt()`                                                                                                           | AIGC prompt content-safety scan         |

## Documentation

| Document                               | Content                                                                                 |
| -------------------------------------- | --------------------------------------------------------------------------------------- |
| [API Reference](docs/api-reference.md) | Complete method reference — parameters, return types, `BillingDetail` fields            |
| [GraphQL Guide](docs/graphql-guide.md) | Queries, filters, analytics, introspection, delivery logs                               |
| [Webhook Guide](docs/webhook-guide.md) | Signature verification, event types, event data fields, key resolution, retry mechanism |
| [Changelog](CHANGELOG.md)              | Version history and migration guides                                                    |

## Exports

### Classes & Functions

| Export              | Description                                 |
| ------------------- | ------------------------------------------- |
| `WaffoPancake`      | SDK client with auto-signed requests        |
| `WaffoPancakeError` | API error with status and call-stack errors |
| `verifyWebhook`     | Standalone webhook signature verification   |

### Enums

| Export                       | Values                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Environment`                | `Test`, `Prod`                                                                                                                                                                                                                                                                                                                                               |
| `TaxCategory`                | `DigitalGoods`, `SaaS`, `Software`, `Ebook`, `OnlineCourse`, `Consulting`, `ProfessionalService`                                                                                                                                                                                                                                                             |
| `ChangeTiming`               | `Immediate`, `NextPeriod`                                                                                                                                                                                                                                                                                                                                    |
| `BillingPeriod`              | `Weekly`, `Monthly`, `Quarterly`, `Yearly`                                                                                                                                                                                                                                                                                                                   |
| `ProductVersionStatus`       | `Active`, `Inactive`                                                                                                                                                                                                                                                                                                                                         |
| `EntityStatus`               | `Active`, `Inactive`, `Suspended`                                                                                                                                                                                                                                                                                                                            |
| `StoreRole`                  | `Owner`, `Admin`, `Member`                                                                                                                                                                                                                                                                                                                                   |
| `OnetimeOrderStatus`         | `Pending`, `Completed`, `Canceled`                                                                                                                                                                                                                                                                                                                           |
| `SubscriptionOrderStatus`    | `Pending`, `Active`, `Canceling`, `PastDue`, `Closed`, `Canceled`, `Expired`                                                                                                                                                                                                                                                                                 |
| `PaymentStatus`              | `Pending`, `Succeeded`, `Failed`, `Canceled`                                                                                                                                                                                                                                                                                                                 |
| `RefundTicketStatus`         | `Pending`, `Approved`, `Rejected`, `Processing`, `Succeeded`, `Failed`                                                                                                                                                                                                                                                                                       |
| `RefundStatus`               | `Succeeded`, `Failed`                                                                                                                                                                                                                                                                                                                                        |
| `MediaType`                  | `Image`, `Video`                                                                                                                                                                                                                                                                                                                                             |
| `CheckoutSessionProductType` | `Onetime`, `Subscription`                                                                                                                                                                                                                                                                                                                                    |
| `ErrorLayer`                 | `Gateway`, `User`, `Store`, `Product`, `Order`, `Ticket`, `GraphQL`, `Resource`, `Email`                                                                                                                                                                                                                                                                     |
| `WebhookEventType`           | `OrderCompleted`, `SubscriptionActivated`, `SubscriptionPaymentSucceeded`, `SubscriptionRenewed`, `SubscriptionRecovered`, `SubscriptionPlanChanged`, `SubscriptionPlanChangeScheduled`, `SubscriptionPlanChangeFailed`, `SubscriptionCanceling`, `SubscriptionUncanceled`, `SubscriptionCanceled`, `SubscriptionPastDue`, `RefundSucceeded`, `RefundFailed` |

### Types

Key types: `WaffoPancakeConfig`, `AuthenticatedCheckoutParams`, `AuthenticatedCheckoutResult`, `AnonymousCheckoutParams`, `CreatePlanChangeSessionParams`, `AuthenticatedPlanChangeParams`, `CustomerPlanChangeParams`, `CheckoutSessionResult`, `CashierLanguage`, `Store`, `OnetimeProductDetail`, `SubscriptionProductDetail`, `WebhookEvent<T>`, `WebhookEventData`, `GraphQLResponse<T>`, and 30+ more. `WebhookEventData` includes rich fields organized by section: order info, amounts, product, payment, subscription, and refund (conditional by event type). See [API Reference](docs/api-reference.md#types) for the full list.

## Development

```bash
npm run lint            # ESLint 9 (TypeScript ESLint + import order + JSDoc)
npm run test            # Vitest
npm run test:watch      # Vitest in watch mode
npm run test:coverage   # Vitest with v8 coverage
npm run build           # tsup → ESM + CJS + DTS
```

## Project Structure

```
src/
├── index.ts               # Unified export entry
├── client.ts              # WaffoPancake main class
├── http-client.ts         # HTTP client (API Key, auto-signing)
├── customer-http-client.ts   # HTTP client (Bearer token, customer self-service)
├── signing.ts             # RSA-SHA256 request signing
├── errors.ts              # WaffoPancakeError
├── webhooks.ts            # Webhook verification (embedded keys)
├── validation.ts          # Client-side input validation
├── types.ts               # Type definitions & enums
├── __tests__/             # Test suite
└── resources/             # API resource classes
    ├── auth.ts
    ├── stores.ts
    ├── store-merchants.ts
    ├── onetime-products.ts
    ├── subscription-products.ts
    ├── subscription-product-groups.ts
    ├── customer.ts
    ├── orders.ts
    ├── checkout.ts
    ├── checkout-anonymous.ts
    ├── checkout-authenticated.ts
    ├── graphql.ts
    └── webhooks.ts
docs/
├── api-reference.md       # Complete API reference
├── graphql-guide.md       # GraphQL queries & analytics
└── webhook-guide.md       # Webhook verification guide
```

## License

MIT

---
_Source: https://npm.io/package/@waffo/pancake-ts · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
