> ## 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 集成

> Waffo Pancake 官方 TypeScript SDK 完整参考 — 涵盖商店、产品、收银台、Webhook、订阅和 GraphQL

### 简单版本

直接告诉你的 AI 助手：

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

就这一句。把这个页面作为 AI 集成主入口，需要精确 `SKILL.md` 时再直接打开下面的官方 Skill 入口。

### 完整版本

需要端到端完整集成和测试：

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/zh/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（将此值用作 `WAFFO_MERCHANT_ID`，不是 `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="官方 Waffo Pancake Skill" icon="download" href="/zh/integrate/skill">
  这是 AI 集成页内的官方 Skill 入口。打开后可查看、复制或下载团队当前实际使用的标准 `SKILL.md` 文件。
</Card>

***

`@waffo/pancake-ts` 是 Waffo Pancake 官方服务端 TypeScript SDK，负责请求签名、收银台会话创建、Webhook 验证和 GraphQL 查询。

## AI 编码工作流

当你已经明确业务模式，但希望代理把它整理成一套清晰的 Waffo 产品结构和实施方案时，AI 编码助手最有价值。

典型场景：

* 将官网定价页映射成 Waffo 的产品、订阅产品组和动态定价流程
* 判断哪些收费应建模为订阅，哪些应保留为一次性收费
* 设计基于 `priceSnapshot` 的动态定价方案
* 批量生成产品命名、元数据、上线清单
* 审查现有产品目录，找出命名、分组和生产发布上的问题

### 推荐工作流

<Steps>
  <Step title="先描述业务模型">
    说明你卖什么、如何收费、哪些部分是固定价格、哪些部分按用量或报价动态计算。
  </Step>

  <Step title="让代理输出产品规划">
    让它把你的收费模型映射成一次性产品、订阅产品、产品组和动态定价流程。
  </Step>

  <Step title="人工审核">
    确认产品命名、计费周期、商品类别，以及哪些项目应该保持为一次性收费。
  </Step>

  <Step title="落地执行">
    用输出结果在控制台中创建产品，或进一步生成 SDK / API 集成代码。
  </Step>
</Steps>

### 推荐提示词

#### 1. 把定价页转成 Waffo 产品结构

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

请把下面的定价模型整理成 Waffo Pancake 的产品结构：
- Starter：$19/月
- Pro：$59/月
- Scale：按年定制报价
- 超额用量：每额外 credit 收费 $0.20
- Onboarding Fee：一次性 $499

请输出：
1. 哪些应该建成订阅产品
2. 哪些应该建成一次性产品
3. 哪些订阅产品应该放进同一个产品组
4. 哪些流程需要用 priceSnapshot 做动态定价
5. 推荐的产品命名、商品类别和测试到生产的发布顺序
```

#### 2. 规划动态定价

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

我已经在 Waffo Pancake 里做了订阅业务，现在要增加超额计费。

请帮我设计：
1. 应该先创建什么基础一次性产品
2. 什么时候使用 priceSnapshot
3. 服务端在创建收银台前需要计算哪些数据
4. 如何向团队解释“动态定价”和“订阅计费”的边界
```

#### 3. 审查现有产品目录

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

请审查我现有的 Waffo 产品目录，并告诉我：
1. 哪些命名不清晰
2. 哪些订阅产品应该组成产品组
3. 哪些场景更适合动态定价，而不是固定价格
4. 哪些收费虽然属于订阅业务，但仍应保留为一次性收费
5. 生产环境应该先发布哪些产品
```

### 实务判断规则

| 场景        | 推荐建模                                    |
| --------- | --------------------------------------- |
| 固定公开价格    | 直接写在产品价格里                               |
| 运行时计算金额   | 创建 Checkout Session 时传入 `priceSnapshot` |
| 周期性收费方案   | 订阅产品                                    |
| 多个订阅套餐    | 每个套餐一个订阅产品，再用产品组组织                      |
| 开通费、充值包   | 一次性产品                                   |
| 订阅客户的超额收费 | 一次性产品 + 动态定价                            |

<Note>
  “订阅型业务”同时包含订阅产品和一次性收费，是完全正常的。建模要服从收费事件，而不是服从公司标签。
</Note>

### 不建议这样做

* 不要把私钥或生产环境密钥直接贴进提示词
* 不要在未经人工审核的情况下让 AI 直接发布生产产品
* 不要把所有价格变化都建成新产品；如果金额在运行时计算，应优先考虑 `priceSnapshot`
* 不要把超额计费硬塞进订阅产品；如果它是事件型收费，就应该是一笔一次性收费

## 常见陷阱 — 先读这里

这些是导致集成失败的常见错误。写代码之前先过一遍。

| 陷阱                                    | 为什么会出问题                                                                     | 解决方法                                                                                            |
| ------------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| 以 JSON 方式读取 Webhook body              | `request.json()` 会重新序列化 body，改变空白字符。签名验证对比的是**原始字节**。                       | 始终使用 `request.text()`（App Router / Hono）或 `express.raw()`（Express）。                             |
| 使用 localtunnel 做 Webhook 隧道           | localtunnel 会丢弃自定义 HTTP header。`X-Waffo-Signature` 根本到不了你的处理程序。             | 改用 `cloudflared tunnel --url http://localhost:3000`。                                            |
| 忘记调用 `.publish()`                     | 产品默认创建在 `test` 环境。为未发布产品创建生产收银台会话会静默失败。                                     | 上线前调用 `client.onetimeProducts.publish({ id })` 或 `client.subscriptionProducts.publish({ id })`。 |
| 访问 `result` 而非 `result.data`（GraphQL） | GraphQL 客户端返回 `{ data: T \| null, errors?: [...] }`。字段嵌套在 `.data` 下。        | 始终解构：`const stores = result.data?.stores ?? []`。                                                |
| GraphQL 变量使用 `$id: ID!`               | 后端使用 `$id: String!`，不是 `$id: ID!`。类型错误会静默返回 `null`。                         | ID 变量始终声明为 `String!`。                                                                           |
| 产品组更新中的 `productIds` 是完整替换            | 调用 `subscriptionProductGroups.update({ productIds: [...] })` 会替换整个列表，而不是追加。 | 始终传入完整的目标列表，而不是只传新增项。                                                                           |

***

## 使用场景

Waffo Pancake 是一个 Merchant of Record（MoR）支付平台。以下项目类型适合使用 SDK：

* **SaaS 订阅计费** — 月付/年付计划，支持升降级（例如：Free/Pro/Team 层级）
* **数字商品销售** — 电子书、模板、课程、许可证的一次性购买
* **按次付费** — 按下载、API 调用或生成报告等操作收费
* **混合模式** — 订阅 + 一次性购买组合

| 项目类型                            | 付费模式          | 需要创建的产品                                        |
| ------------------------------- | ------------- | ---------------------------------------------- |
| AI Skills 市场                    | 按次下载 + Pro 订阅 | 1 个一次性产品（\$0.99/次下载）+ 2 个订阅产品（月付/年付）           |
| 在线课程平台                          | 按课程一次性购买      | 每门课程 1 个一次性产品（$29–$199）                        |
| SaaS 工具（Starter/Pro/Enterprise） | 订阅层级          | 3 个订阅产品 + 1 个产品组用于计划切换                         |
| 设计模板商店                          | 按模板一次性购买      | 每个模板 1 个一次性产品，或 1 个共享产品配合 `priceSnapshot` 覆盖价格 |
| API 服务（用量额度）                    | 额度包 + 订阅      | 每个额度包 1 个一次性产品 + 订阅提供月度配额                      |

***

## 安装与配置

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

仅限服务端使用。Node.js 18+。零依赖。

```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!,
});
```

需要两个环境变量 — 注册时提供：

```
WAFFO_MERCHANT_ID=<你的商户ID>
WAFFO_PRIVATE_KEY=<你的RSA私钥>
```

`WAFFO_MERCHANT_ID` 指的是 **Merchant ID（商户 ID）**，不是 `storeId`，也不是 URL 里的商店标识。`storeId` 仍然是当前 API 模型的一部分，在商店和商品管理流程里会继续使用，不要把这两个概念混在一起。

对第一版能跑通的接入来说，只需要这两个环境变量：`WAFFO_MERCHANT_ID` 和 `WAFFO_PRIVATE_KEY`。Store ID 和 Product ID 属于运行时值，可以放在代码、应用配置或你自己的数据库里。

### PEM 私钥处理

**转义换行符**（最简单）：

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

**Base64 编码**（推荐用于 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");
```

**文件路径**（本地开发）：

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

***

## 快速开始：路径 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 ID 和 Product ID 属于后续运行时配置。放在你应用自己的配置层即可，不必强制做成环境变量。

如果一个商户名下有多个店铺，创建商品前先确认应该落在哪个店铺，不要直接猜测目标店铺。

## 快速开始：路径 B

如果产品已在控制台中创建，复制产品 ID 后即可直接创建收银台会话。在这个流程里，你仍然只需要前面的两个环境变量：

```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 参考

### 商店

```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" });
```

### 一次性产品

```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 选项：** `digital_goods` | `saas` | `software` | `ebook` | `online_course` | `consulting` | `professional_service`

### 订阅产品

```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
```

### 订阅产品组

产品组支持共享试用期和订阅计划之间的切换。

```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" });
```

### 收银台会话

```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
```

### 订单级参数与优先级

通过 `createSession` 传递的参数可以覆盖产品级设置。理解优先级关系对 AI 集成至关重要：

| 参数                 | 作用             | 优先级                              |
| ------------------ | -------------- | -------------------------------- |
| `priceSnapshot`    | 覆盖产品价格（动态定价场景） | **最高** — 忽略产品设定价格                |
| `currency`         | 指定收银台货币        | 必填 — 从产品 prices 中选取对应币种          |
| `buyerEmail`       | 预填消费者邮箱        | 可选 — 跳过收银台页邮箱输入步骤                |
| `billingDetail`    | 预填账单信息（国家、税号等） | 可选 — 跳过收银台页地址输入                  |
| `successUrl`       | 支付成功跳转地址       | 覆盖产品级 successUrl                 |
| `metadata`         | 自定义键值对（内部订单号等） | 透传到 webhook event.data           |
| `withTrial`        | 是否启用试用期        | 覆盖产品级试用设置                        |
| `expiresInSeconds` | 会话过期时间         | 默认 45 分钟，最长 7 天                  |
| `darkMode`         | 收银台页暗色模式       | `true`=暗色 / `false`=亮色 / 省略=门店默认 |

<Warning>
  **`priceSnapshot` 是动态定价的核心参数。** 当传入 `priceSnapshot` 时，产品上设定的价格会被完全忽略。这适用于：按用量阶梯定价、优惠码动态折扣、A/B 测试不同价格点等场景。
</Warning>

```typescript theme={"system"}
// 动态定价示例：根据用量阶梯覆盖价格
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" }, // 覆盖产品价格
  buyerEmail: "user@example.com",
  metadata: { internalOrderId: "ORD-2024-001", tier: "growth" },
  successUrl: "https://myapp.com/purchase/success",
});
```

### Webhook 集成要点

集成 Webhook 时需要注意以下关键点：

| 要点           | 说明                                                 |
| ------------ | -------------------------------------------------- |
| **原始 body**  | 必须用 `request.text()` 读取，不能用 `.json()` — 签名验证依赖原始字节 |
| **幂等处理**     | 用 `event.id`（投递 ID）去重，同一事件可能重试多次                   |
| **立即返回 200** | 先返回 200 OK，再异步处理业务逻辑。超时会触发重试                       |
| **环境区分**     | `event.mode` 为 `"test"` 或 `"prod"`，确保测试事件不触发生产逻辑   |
| **重试机制**     | 非 2xx 或超时触发重试（默认 3 次，指数退避）                         |

**典型 Webhook 处理模式：**

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

    // 幂等检查
    if (await isDuplicate(event.id)) return new Response("OK");
    await markProcessed(event.id);

    // 按事件类型分发
    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 });
  }
}
```

### 取消订阅

```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 查询

只读查询。ID 变量使用 `String!`（不是 `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 验证

SDK 内置 test 和 prod 两个环境的公钥。验证只需一次函数调用。

### 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);
  }
});
```

### 验证选项

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

### 事件类型

| 事件                               | 触发条件             |
| -------------------------------- | ---------------- |
| `order.completed`                | 一次性支付成功          |
| `subscription.activated`         | 首次订阅支付成功         |
| `subscription.payment_succeeded` | 续费支付成功           |
| `subscription.canceling`         | 发起取消（当前周期结束前仍有效） |
| `subscription.uncanceled`        | 撤回取消             |
| `subscription.updated`           | 计划变更（升级/降级）      |
| `subscription.canceled`          | 订阅完全终止           |
| `subscription.past_due`          | 续费支付失败           |
| `refund.succeeded`               | 退款完成             |
| `refund.failed`                  | 退款失败             |

### 事件结构

```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;     // 美元金额（如 9.99）
    taxAmount: number;
    productName: string;
  };
}
```

### 配置 Webhook URL

一个门店可以注册多条 webhook，每条投递到不同渠道（`http`、`feishu`、`discord`、`telegram`、`slack`）。每个渠道和环境对应一条记录：

```typescript theme={"system"}
// HTTP — 测试环境
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 — 生产环境
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,
});

// 更新已有 webhook（events / url）
await client.webhooks.update({
  id: "WBH_xxx",
  events: ["order.completed"],
});

// 移除 webhook（硬删除）
await client.webhooks.remove({ id: "WBH_xxx" });
```

如需查询 webhook 列表，使用 GraphQL `Store.storeWebhooks` 字段 -- 这是唯一的查询入口。

***

## 错误处理

```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[0]` 是根本原因（最深层），`errors[n]` 是最外层调用者。

***

## 开发提示

1. **Webhook 隧道** — 使用 `cloudflared`，不要用 localtunnel（见陷阱表）。

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

2. **幂等性自动处理** — SDK 根据 `merchantId + path + body` 自动生成确定性键。重试是安全的。

3. **Test → Prod 工作流** — 产品默认创建在 test 环境。使用 `.publish()` 发布到生产环境。Webhook 事件包含 `mode: "test" | "prod"`，你的处理程序可以据此区分。

***

## 快速上手清单

1. `npm install @waffo/pancake-ts`
2. 配置 `WAFFO_MERCHANT_ID` 和 `WAFFO_PRIVATE_KEY` 环境变量（在控制台 → API 与开发页面顶部，点复制按钮获取）
3. 初始化 `new WaffoPancake({ merchantId, privateKey })`
4. 创建或引用商店
5. 如果商户名下有多个店铺，先确认商品要创建到哪个店铺
6. 创建或引用产品
7. 创建收银台：`client.checkout.createSession(...)` → 重定向到 `checkoutUrl`
8. 使用测试卡 `4576750000000110`（成功）或 `4576750000000220`（拒绝）在沙盒中测试
9. 处理 Webhook：`verifyWebhook(rawBody, sig)` — **必须使用 `request.text()`**
10. 配置 Webhook URL：`client.webhooks.add({ storeId, channel: "http", url, events, testMode })`
