> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AI Integration

> Complete reference for the @waffo/pancake-ts SDK — covers store setup, products, checkout, webhooks, subscriptions, and GraphQL queries

### The Simple Version

Tell your AI assistant:

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/integrate/skill, and integrate Waffo Pancake payments into the current project.
```

That's it. Use this page as the AI integration entry point, then open the official skill file below when you need the exact `SKILL.md`.

### The Full Version

For a complete integration with end-to-end tests:

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/integrate/skill, and use Waffo Pancake SDK to
integrate Waffo Pancake payments into the current project and run through
the full checkout flow:
1. Get Merchant ID from Dashboard → API & Development → top of the page → copy button (use this as `WAFFO_MERCHANT_ID`, not `storeId`)
2. Create an API Key from Dashboard → API & Development → API Keys
3. Use only `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY` as required env vars for the first working integration
4. Install @waffo/pancake-ts SDK
5. Create checkout and webhook endpoints
6. Test with card 4576750000000110
7. Verify webhook receives order.completed event
Use test environment.
```

<Card title="Official Waffo Pancake Skill" icon="download" href="/integrate/skill">
  Open the official skill file from the AI Integration page to view, copy, or download the exact `SKILL.md` used by the team.
</Card>

***

The `@waffo/pancake-ts` SDK is the official server-side TypeScript client for the Waffo Pancake API. It handles request signing, checkout session creation, webhook verification, and GraphQL queries.

## AI Coding Workflow

AI coding agents are most useful when you already understand the business model but want help turning it into a clean Waffo catalog and implementation plan.

Typical tasks:

* Convert a pricing page into Waffo products and product groups
* Decide which offers should be subscription products vs one-time charges
* Design dynamic pricing flows with `priceSnapshot`
* Batch-generate product definitions, metadata, and rollout checklists
* Review an existing catalog for naming, plan structure, and production readiness

### Recommended Workflow

<Steps>
  <Step title="Describe the business model">
    Explain what you sell, how customers are charged, and which parts are fixed-price versus usage-based.
  </Step>

  <Step title="Ask for a catalog plan">
    Have the agent map your offers into one-time products, subscription products, product groups, and optional dynamic pricing flows.
  </Step>

  <Step title="Review the output">
    Confirm naming, billing periods, tax categories, and whether any add-ons should remain one-time charges.
  </Step>

  <Step title="Implement">
    Use the output to create products in the Dashboard or to generate SDK/API integration code.
  </Step>
</Steps>

### Prompt Templates

#### 1. Turn a Pricing Page into Waffo Products

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt.

I need you to turn this pricing model into a Waffo Pancake catalog:
- Starter: $19/month
- Pro: $59/month
- Scale: custom annual contract
- Overage: $0.20 per extra credit
- Optional onboarding fee: $499 one time

Please output:
1. Which items should be subscription products
2. Which items should be one-time products
3. Which subscription products should be grouped together
4. Which flows require dynamic pricing via priceSnapshot
5. Recommended product names, tax categories, and environment rollout order
```

#### 2. Plan Dynamic Pricing

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt.

I already have a subscription business in Waffo Pancake. I need to add dynamic pricing for overage billing.

Please design:
1. The base one-time product I should create
2. When to use priceSnapshot
3. What data should be calculated on my server before checkout
4. How to explain this clearly to my team so they do not confuse it with subscription billing
```

#### 3. Review an Existing Catalog

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt.

Review this Waffo product catalog and tell me:
1. Which names are unclear
2. Which subscription products should be grouped
3. Where dynamic pricing should replace fixed pricing
4. Which products should stay one-time even though the business is subscription-led
5. What should be published to production first
```

### Practical Rules

| Situation                                  | Recommended model                                 |
| ------------------------------------------ | ------------------------------------------------- |
| Fixed public price                         | Product price stored on the product               |
| Runtime-calculated amount                  | Checkout session with `priceSnapshot`             |
| Recurring plan                             | Subscription product                              |
| Multiple subscription tiers                | One subscription product per tier + product group |
| Setup fee or credits top-up                | One-time product                                  |
| Overage charge for a subscription customer | One-time product with dynamic pricing             |

<Note>
  It is normal for a subscription-led business to create both subscription products and one-time charges. The charging model should match the business event, not the company label.
</Note>

### What To Avoid

* Do not paste private keys or production secrets into prompts
* Do not let an AI agent publish products to production without review
* Do not model every pricing variation as a separate product if the final amount is computed at runtime
* Do not force overage billing into subscription products when the charge is event-based

## Gotchas — Read This First

These are the mistakes that break integrations. Read before writing any code.

| Gotcha                                                 | Why It Breaks                                                                                                                     | Fix                                                                                                               |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Reading webhook body as JSON                           | `request.json()` re-serializes the body, changing whitespace. Signature verification compares against the **original raw bytes**. | Always use `request.text()` (App Router / Hono) or `express.raw()` (Express).                                     |
| Using localtunnel for webhooks                         | localtunnel strips custom HTTP headers. `X-Waffo-Signature` never reaches your handler.                                           | Use `cloudflared tunnel --url http://localhost:3000` instead.                                                     |
| Forgetting `.publish()`                                | Products are created in `test` environment by default. Production checkout sessions for unpublished products will fail silently.  | Call `client.onetimeProducts.publish({ id })` or `client.subscriptionProducts.publish({ id })` before going live. |
| Accessing `result` instead of `result.data` in GraphQL | The GraphQL client returns `{ data: T \| null, errors?: [...] }`. Fields are nested under `.data`.                                | Always destructure: `const stores = result.data?.stores ?? []`.                                                   |
| Using `$id: ID!` in GraphQL variables                  | Backend uses `$id: String!`, not `$id: ID!`. Using the wrong type silently returns `null`.                                        | Always declare ID variables as `String!`.                                                                         |
| `productIds` in group update is full replacement       | Calling `subscriptionProductGroups.update({ productIds: [...] })` replaces the entire list — it does not append.                  | Always pass the complete desired list, not just new additions.                                                    |

***

## Use Cases

Waffo Pancake is a merchant-of-record payment platform. The SDK fits projects that need:

* **SaaS subscription billing** — monthly/yearly plans with upgrade/downgrade (e.g., Free/Pro/Team tiers)
* **Digital product sales** — one-time purchases for e-books, templates, courses, licenses
* **Per-usage payments** — charge per download, API call, or generated report
* **Hybrid models** — subscriptions + one-time purchases combined

| Project Type                  | Payment Model                   | Products to Create                                                         |
| ----------------------------- | ------------------------------- | -------------------------------------------------------------------------- |
| AI Skills marketplace         | Per-download + Pro subscription | 1 one-time (\$0.99/download) + 2 subscriptions (monthly/yearly)            |
| Online course platform        | One-time per course             | 1 one-time per course ($29–$199)                                           |
| SaaS (Starter/Pro/Enterprise) | Subscription tiers              | 3 subscriptions + 1 product group for plan switching                       |
| Template shop                 | One-time per template           | 1 one-time per template, or 1 shared product with `priceSnapshot` override |
| API credits                   | Credit packs + subscription     | 1 one-time per pack + subscription for monthly quota                       |

***

## Installation & Setup

```bash theme={"system"}
npm install @waffo/pancake-ts
```

Server-side only. Node.js 18+. Zero dependencies.

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

const client = new WaffoPancake({
  merchantId: process.env.WAFFO_MERCHANT_ID!,
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
```

Two env vars are required — provided at signup:

```
WAFFO_MERCHANT_ID=<your-merchant-id>
WAFFO_PRIVATE_KEY=<your-rsa-private-key>
```

`WAFFO_MERCHANT_ID` means your **Merchant ID**, not `storeId` and not a store identifier from a URL. `storeId` is still part of the current API model for store and product management flows, so do not confuse the two.

For the first working integration, only these two env vars need to exist: `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY`. Store IDs and Product IDs are runtime values you can keep in code, app config, or your own database.

### PEM Key Handling

**Escaped newlines** (simplest):

```
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"
```

**Base64** (recommended for CI/CD):

```bash theme={"system"}
cat private.pem | base64 | tr -d '\n'
```

```typescript theme={"system"}
const privateKey = Buffer.from(process.env.WAFFO_PRIVATE_KEY_BASE64!, "base64").toString("utf-8");
```

**File path** (local dev):

```typescript theme={"system"}
import { readFileSync } from "fs";
const privateKey = readFileSync("./keys/private.pem", "utf-8");
```

***

## Quick Start: Path A

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

// 2. Create products
const { product: monthly } = await client.subscriptionProducts.create({
  storeId: store.id,
  name: "Pro Monthly",
  billingPeriod: "monthly",
  prices: { USD: { amount: "9.99", taxIncluded: true, taxCategory: "saas" } },
});

const { product: yearly } = await client.subscriptionProducts.create({
  storeId: store.id,
  name: "Pro Yearly",
  billingPeriod: "yearly",
  prices: { USD: { amount: "99.00", taxIncluded: true, taxCategory: "saas" } },
});

// 3. Create a checkout session
const session = await client.checkout.createSession({
  productId: monthly.id,
  productType: "subscription",
  currency: "USD",
});
// Redirect customer to session.checkoutUrl
```

Store IDs and Product IDs are follow-up values. Save them wherever your app keeps runtime configuration; they do not need to be env vars unless you want that convention.

If a merchant has multiple stores, confirm which store should own the product before creating it. Do not guess the target store.

## Quick Start: Path B

If products already exist in the Dashboard, copy the Product ID and go straight to checkout. In this flow, you still only need the same two env vars above:

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "PROD_xxx_from_dashboard",
  productType: "subscription",
  currency: "USD",
  buyerEmail: "customer@example.com",
  successUrl: "https://myapp.com/welcome",
});
// Redirect customer to session.checkoutUrl
```

***

## API Reference

### Stores

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

// Update (partial — only provided fields change)
const { store } = await client.stores.update({
  id: "store_id",
  name: "New Name",
  supportEmail: "help@example.com",
  website: "https://example.com",
});

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

### One-Time Products

```typescript theme={"system"}
const { product } = await client.onetimeProducts.create({
  storeId: "store_id",
  name: "E-Book",
  description: "A great e-book",
  prices: {
    USD: { amount: 29.00, taxIncluded: false, taxCategory: "digital_goods" },
  },
  successUrl: "https://example.com/thanks",
  metadata: { sku: "EB-001" },
});

// Update (creates new immutable version)
const { product } = await client.onetimeProducts.update({
  id: "product_id",
  name: "E-Book v2",
  prices: { USD: { amount: 39.00, taxIncluded: false, taxCategory: "digital_goods" } },
});

// Publish test → production (required before going live)
const { product } = await client.onetimeProducts.publish({ id: "product_id" });

// Activate / deactivate
const { product } = await client.onetimeProducts.updateStatus({
  id: "product_id",
  status: "inactive", // or "active"
});
```

**taxCategory options:** `digital_goods` | `saas` | `software` | `ebook` | `online_course` | `consulting` | `professional_service`

### Subscription Products

```typescript theme={"system"}
const { product } = await client.subscriptionProducts.create({
  storeId: "store_id",
  name: "Pro Monthly",
  billingPeriod: "monthly", // "weekly" | "monthly" | "quarterly" | "yearly"
  prices: { USD: { amount: 9.99, taxIncluded: true, taxCategory: "saas" } },
});

// update, publish, updateStatus — same pattern as one-time products
```

### Subscription Product Groups

Groups enable shared trials and plan switching between subscription products.

```typescript theme={"system"}
const { group } = await client.subscriptionProductGroups.create({
  storeId: "store_id",
  name: "Pro Plans",
  rules: { sharedTrial: true },
  productIds: ["monthly_product_id", "yearly_product_id"],
});

// Update (productIds is FULL REPLACEMENT, not append)
await client.subscriptionProductGroups.update({
  id: "group_id",
  productIds: ["monthly_id", "quarterly_id", "yearly_id"],
});

// Publish to production
await client.subscriptionProductGroups.publish({ id: "group_id" });

// Delete (physical delete, not soft-delete)
await client.subscriptionProductGroups.delete({ id: "group_id" });
```

### Checkout Sessions

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "product_id",
  productType: "onetime",       // "onetime" | "subscription"
  currency: "USD",
  buyerEmail: "buyer@example.com",           // optional, pre-fills email
  successUrl: "https://example.com/thanks",  // redirect after payment
  metadata: { orderId: "internal-123" },     // custom key-value pairs
  // Optional overrides:
  priceSnapshot: { amount: 19.99, taxIncluded: true, taxCategory: "saas" },
  billingDetail: { country: "US", isBusiness: false },
  expiresInSeconds: 3600,  // default: 7 days
});

// session.checkoutUrl  — redirect customer here
// session.sessionId    — for tracking
// session.expiresAt    — ISO 8601 expiry
```

### Order-Level Parameters & Priority

Parameters passed to `createSession` can override product-level settings. Understanding the priority hierarchy is essential for AI integrations:

| Parameter          | Purpose                                          | Priority                                                     |
| ------------------ | ------------------------------------------------ | ------------------------------------------------------------ |
| `priceSnapshot`    | Override product price (dynamic pricing)         | **Highest** — ignores the product's set price                |
| `currency`         | Specify checkout currency                        | Required — selects the matching currency from product prices |
| `buyerEmail`       | Pre-fill consumer email                          | Optional — skips the email input step on the checkout page   |
| `billingDetail`    | Pre-fill billing info (country, tax ID, etc.)    | Optional — skips the address input on the checkout page      |
| `successUrl`       | Redirect URL after successful payment            | Overrides product-level successUrl                           |
| `metadata`         | Custom key-value pairs (internal order ID, etc.) | Passed through to webhook event.data                         |
| `withTrial`        | Enable trial period                              | Overrides product-level trial settings                       |
| `expiresInSeconds` | Session expiration time                          | Default 45 minutes, max 7 days                               |
| `darkMode`         | Checkout page dark mode                          | `true`=dark / `false`=light / omit=store default             |

<Warning>
  **`priceSnapshot` is the key parameter for dynamic pricing.** When `priceSnapshot` is provided, the price set on the product is completely ignored. Use cases include: usage-based tiered pricing, dynamic coupon discounts, A/B testing different price points, and more.
</Warning>

```typescript theme={"system"}
// Dynamic pricing example: override price based on usage tier
const session = await client.checkout.createSession({
  storeId: "store_id",
  productId: "api-credits-product-id",
  productType: "onetime",
  currency: "USD",
  priceSnapshot: { amount: 49.00, taxIncluded: true, taxCategory: "saas" }, // overrides product price
  buyerEmail: "user@example.com",
  metadata: { internalOrderId: "ORD-2024-001", tier: "growth" },
  successUrl: "https://myapp.com/purchase/success",
});
```

### Webhook Integration Notes

Key points to keep in mind when integrating webhooks:

| Point                       | Description                                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Raw body**                | Must use `request.text()` to read the body, not `.json()` — signature verification relies on raw bytes |
| **Idempotent handling**     | Use `event.id` (delivery ID) for deduplication — the same event may be retried multiple times          |
| **Return 200 immediately**  | Return 200 OK first, then process business logic asynchronously. Timeouts trigger retries              |
| **Environment distinction** | `event.mode` is `"test"` or `"prod"` — ensure test events don't trigger production logic               |
| **Retry mechanism**         | Non-2xx or timeout triggers retries (default 3 times, exponential backoff)                             |

**Typical webhook handler pattern:**

```typescript theme={"system"}
import { verifyWebhook } from "@waffo/pancake-ts";

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);

    // Idempotency check
    if (await isDuplicate(event.id)) return new Response("OK");
    await markProcessed(event.id);

    // Dispatch by event type
    switch (event.eventType) {
      case "order.completed":
        await handleOrderCompleted(event.data);
        break;
      case "subscription.activated":
        await handleSubscriptionActivated(event.data);
        break;
      case "subscription.canceled":
        await handleSubscriptionCanceled(event.data);
        break;
      case "subscription.past_due":
        await handlePastDue(event.data);
        break;
    }

    return new Response("OK");
  } catch {
    return new Response("Invalid signature", { status: 401 });
  }
}
```

### Cancel Subscription

```typescript theme={"system"}
const { orderId, status } = await client.orders.cancelSubscription({
  orderId: "order_id",
});
// status: "canceled" (was pending) or "canceling" (active → ends at period end)
```

### GraphQL Queries

Read-only. Use `String!` for ID variables (not `ID!`).

```typescript theme={"system"}
const result = await client.graphql.query<{
  stores: Array<{ id: string; name: string; status: string }>;
}>({
  query: `query { stores { id name status } }`,
});
const stores = result.data?.stores ?? [];

// With variables — note String!, not ID!
const result = await client.graphql.query<{
  onetimeProduct: { id: string; name: string; prices: unknown };
}>({
  query: `query ($id: String!) { onetimeProduct(id: $id) { id name prices } }`,
  variables: { id: "product_id" },
});
const product = result.data?.onetimeProduct;
```

***

## Webhook Verification

The SDK embeds public keys for both environments. Verification is one function call.

### Next.js App Router

```typescript theme={"system"}
import { verifyWebhook } from "@waffo/pancake-ts";

export async function POST(request: Request) {
  const body = await request.text(); // MUST be raw text, not .json()
  const sig = request.headers.get("x-waffo-signature");
  try {
    const event = verifyWebhook(body, sig);
    // event.eventType, event.data, event.storeId, event.mode
    return new Response("OK");
  } catch {
    return new Response("Invalid signature", { status: 401 });
  }
}
```

### Express

```typescript theme={"system"}
import express from "express";
import { verifyWebhook } from "@waffo/pancake-ts";

// MUST use express.raw(), not express.json()
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,
    );
    res.status(200).send("OK");
  } catch {
    res.status(401).send("Invalid signature");
  }
});
```

### Hono

```typescript theme={"system"}
import { verifyWebhook } from "@waffo/pancake-ts";

app.post("/webhooks", async (c) => {
  const body = await c.req.text(); // raw text
  const sig = c.req.header("x-waffo-signature");
  try {
    const event = verifyWebhook(body, sig);
    return c.text("OK");
  } catch {
    return c.text("Invalid signature", 401);
  }
});
```

### Verification Options

```typescript theme={"system"}
// Explicit environment (skip auto-detection)
const event = verifyWebhook(body, sig, { environment: "prod" });

// Custom replay tolerance (default: 5 minutes)
const event = verifyWebhook(body, sig, { toleranceMs: 600000 });

// Disable replay protection (not recommended in production)
const event = verifyWebhook(body, sig, { toleranceMs: 0 });
```

### Event Types

| Event                            | Trigger                                    |
| -------------------------------- | ------------------------------------------ |
| `order.completed`                | One-time payment succeeded                 |
| `subscription.activated`         | First subscription payment succeeded       |
| `subscription.payment_succeeded` | Renewal payment succeeded                  |
| `subscription.canceling`         | Cancel initiated (active until period end) |
| `subscription.uncanceled`        | Cancellation withdrawn                     |
| `subscription.updated`           | Plan changed (upgrade/downgrade)           |
| `subscription.canceled`          | Subscription fully terminated              |
| `subscription.past_due`          | Renewal payment failed                     |
| `refund.succeeded`               | Refund completed                           |
| `refund.failed`                  | Refund failed                              |

### Event Shape

```typescript theme={"system"}
interface WebhookEvent {
  id: string;           // Delivery ID (use for idempotent dedup)
  timestamp: string;    // ISO 8601 UTC
  eventType: string;    // e.g. "order.completed"
  eventId: string;      // Business event ID (payment/order ID)
  storeId: string;
  mode: "test" | "prod";
  data: {
    orderId: string;
    buyerEmail: string;
    currency: string;
    amount: number;     // USD dollar amount (e.g. 9.99)
    taxAmount: number;
    productName: string;
  };
}
```

### Configuring Webhook URLs

A store can have multiple webhooks, each delivering to a different channel (`http`, `feishu`, `discord`, `telegram`, `slack`). Register one entry per channel and environment:

```typescript theme={"system"}
// HTTP — Test environment
await client.webhooks.add({
  storeId: "store_id",
  channel: "http",
  url: "https://your-domain.com/api/webhooks",
  events: [
    "order.completed",
    "subscription.activated",
    "subscription.canceled",
    "subscription.past_due",
  ],
  testMode: true,
});

// HTTP — Production environment
await client.webhooks.add({
  storeId: "store_id",
  channel: "http",
  url: "https://your-domain.com/api/webhooks",
  events: [
    "order.completed",
    "subscription.activated",
    "subscription.canceled",
    "subscription.past_due",
  ],
  testMode: false,
});

// Update an existing webhook (events / url)
await client.webhooks.update({
  id: "WBH_xxx",
  events: ["order.completed"],
});

// Remove a webhook (hard delete)
await client.webhooks.remove({ id: "WBH_xxx" });
```

To list webhooks, use the GraphQL `Store.storeWebhooks` field — it is the only query entry point.

***

## Error Handling

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

try {
  await client.stores.create({ name: "My Store" });
} catch (err) {
  if (err instanceof WaffoPancakeError) {
    console.log(err.status);           // HTTP status code
    console.log(err.errors);           // Array of { message, layer }
    console.log(err.errors[0].layer);  // "store" | "product" | "order" | ...
  }
}
```

Errors are ordered by call stack depth: `errors[0]` is the root cause (deepest layer), `errors[n]` is the outermost caller.

***

## Development Tips

1. **Webhook tunneling** — use `cloudflared`, not localtunnel (see Gotchas).

```bash theme={"system"}
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://localhost:3000
```

2. **Idempotency is automatic** — the SDK generates deterministic keys from `merchantId + path + body`. Retries are safe.

3. **Test → Prod workflow** — products default to test. Use `.publish()` to promote. Webhook events include `mode: "test" | "prod"` so your handler can distinguish.

***

## Dashboard UI Glossary

The Dashboard supports English, Chinese, and Japanese. When docs reference a Dashboard location (e.g. "go to Integration"), the label may differ by language. Use this table to find the right menu item.

### Navigation

| English       | 中文 | 日本語       |
| ------------- | -- | --------- |
| Home          | 首页 | ホーム       |
| Products      | 产品 | 商品        |
| Customers     | 客户 | 顧客        |
| Analytics     | 分析 | 分析        |
| Payments      | 付款 | 支払い       |
| Subscriptions | 订阅 | サブスクリプション |
| Revenue       | 收入 | 収益        |
| Integration   | 集成 | インテグレーション |
| Settings      | 设置 | 設定        |

### Key Fields

| English     | 中文     | 日本語      |
| ----------- | ------ | -------- |
| Merchant ID | 商户 ID  | マーチャントID |
| Store ID    | 店铺 ID  | ストアID    |
| API Key     | API 密钥 | APIキー    |
| Private Key | 私钥     | 秘密鍵      |

### Modes & Actions

| English   | 中文   | 日本語    |
| --------- | ---- | ------ |
| Test Mode | 测试模式 | テストモード |
| Live Mode | 生产模式 | 本番モード  |
| Create    | 创建   | 作成     |
| Edit      | 编辑   | 編集     |
| Delete    | 删除   | 削除     |
| Copy      | 复制   | コピー    |
| Save      | 保存   | 保存     |

### Product & Billing

| English      | 中文  | 日本語       |
| ------------ | --- | --------- |
| One-time     | 一次性 | 単発        |
| Subscription | 订阅  | サブスクリプション |
| Weekly       | 每周  | 週間        |
| Monthly      | 每月  | 月間        |
| Quarterly    | 每季度 | 四半期       |
| Yearly       | 每年  | 年間        |

### Statuses

| English          | 中文  | 日本語     |
| ---------------- | --- | ------- |
| Active           | 生效中 | 有効      |
| Awaiting Payment | 待支付 | 支払い待ち   |
| Completed        | 已完成 | 完了      |
| Canceled         | 已取消 | キャンセル済み |
| Expired          | 已过期 | 期限切れ    |

### Where to Find IDs

| Value               | Location in Dashboard                                          |
| ------------------- | -------------------------------------------------------------- |
| `WAFFO_MERCHANT_ID` | API & Development (API 与开发) page → top section → copy button   |
| `Store ID`          | Settings (设置) → Store Profile (店铺资料)                           |
| `Product ID`        | Products (产品) → click a product → shown in URL and detail page |
| `API Key`           | API & Development (API 与开发) → API Keys section → Create Key    |

***

## Quick Start Checklist

1. `npm install @waffo/pancake-ts`
2. Set `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY` env vars (see "Where to Find IDs" above)
3. Initialize `new WaffoPancake({ merchantId, privateKey })`
4. Create or reference a store
5. If the merchant has multiple stores, confirm which store should own the product(s)
6. Create or reference product(s)
7. Create checkout: `client.checkout.createSession(...)` → redirect to `checkoutUrl`
8. Test with card `4576750000000110` (success) or `4576750000000220` (declined) in sandbox
9. Handle webhooks: `verifyWebhook(rawBody, sig)` — **must use `request.text()`**
10. Configure webhook URL: `client.webhooks.add({ storeId, channel: "http", url, events, testMode })`
