> ## 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 の AI ファースト統合ガイド。最新の waffo-pancake スキルと公式 TypeScript SDK に対応

### 推奨エントリーポイント

これは AI アシスト開発の主要入口ページです。AI コーディングエージェントが使用する最新の `waffo-pancake` スキル構造に対応しています：Waffo Pancake の適用場面、統合パスの選び方、商品モデリング、安全なリリース方法をカバーします。

AI コーディングアシスタントに伝えるだけ：

```text theme={"system"}
Read https://docs.waffo.ai/llms-full.txt, load the official Waffo Pancake skill from https://docs.waffo.ai/ja/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/ja/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="/ja/integrate/skill">
  AI インテグレーションページ内の公式 Skill 入口です。開くと、チームが使っている標準 `SKILL.md` を表示、コピー、ダウンロードできます。
</Card>

***

`@waffo/pancake-ts` は Waffo Pancake 決済 API のサーバーサイド TypeScript クライアントです。リクエスト署名、チェックアウトセッション作成、Webhook 検証、GraphQL クエリを処理します。

## よくある落とし穴 — 最初にお読みください

統合を壊すミスをまとめています。コードを書く前に必ず目を通してください。

| 落とし穴                                          | なぜ壊れるか                                                                                   | 修正方法                                                                                                          |
| --------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Webhook ボディを JSON として読み取る                     | `request.json()` はボディを再シリアライズし、空白が変わります。署名検証は**元の生バイト列**と比較します。                          | 常に `request.text()`（App Router / Hono）または `express.raw()`（Express）を使用してください。                                  |
| localtunnel を Webhook に使用する                   | localtunnel はカスタム HTTP ヘッダーを削除します。`X-Waffo-Signature` がハンドラーに届きません。                      | 代わりに `cloudflared tunnel --url http://localhost:3000` を使用してください。                                              |
| `.publish()` を忘れる                             | 商品はデフォルトで `test` 環境に作成されます。未公開商品の本番チェックアウトセッションはサイレントに失敗します。                             | 本番稼働前に `client.onetimeProducts.publish({ id })` または `client.subscriptionProducts.publish({ id })` を呼び出してください。 |
| GraphQL で `result.data` ではなく `result` にアクセスする | 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 決済プラットフォームです。SDK は以下のようなプロジェクトに適しています：

* **SaaS サブスクリプション課金** — 月額/年額プラン、アップグレード/ダウングレード（例：Free/Pro/Team ティア）
* **デジタル商品販売** — 電子書籍、テンプレート、コース、ライセンスの単発購入
* **従量課金** — ダウンロード、API コール、レポート生成ごとに課金
* **ハイブリッドモデル** — サブスクリプション＋単発購入の組み合わせ

| プロジェクトタイプ                    | 課金モデル               | 作成する商品                                              |
| ---------------------------- | ------------------- | --------------------------------------------------- |
| AI Skills マーケットプレイス          | 都度ダウンロード + Pro サブスク | 単発商品1個（\$0.99/ダウンロード）+ サブスク商品2個（月額/年額）              |
| オンラインコースプラットフォーム             | コース単位の単発購入          | コースごとに単発商品1個（$29〜$199）                              |
| SaaS（Starter/Pro/Enterprise） | サブスクティア             | サブスク商品3個 + プラン切替用の商品グループ1個                          |
| デザインテンプレートショップ               | テンプレート単位の単発購入       | テンプレートごとに単発商品1個、または `priceSnapshot` で価格を上書きする共有商品1個 |
| 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!,
});
```

必要な環境変数は2つだけです — サインアップ時に提供されます：

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

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

***

## クイックスタート：ゼロから作成

```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 は後続の実行時設定です。必要ならアプリ設定や自前 DB に保存してください。

マーチャントが複数のストアを持っている場合は、商品を作成する前にどのストアに紐づけるか必ず確認してください。

## クイックスタート：既存商品を使用

ダッシュボードですでに商品を作成済みの場合、Product ID をコピーしてそのままチェックアウトへ：

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: process.env.WAFFO_PRO_MONTHLY_PRODUCT_ID!,
  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`    | 請求情報を事前入力（国、税ID等）     | 任意 — チェックアウトページの住所入力をスキップ              |
| `successUrl`       | 支払い成功後のリダイレクトURL      | 商品レベルの successUrl を上書き                 |
| `metadata`         | カスタムキーバリューペア（内部注文ID等） | 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 には両環境の公開鍵が埋め込まれています。検証は関数呼び出し一回で完了します。

### 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() — express.json() ではなく express.raw() 必須
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 の設定

1 つのストアには複数の Webhook を登録でき、それぞれ異なるチャネル（`http`、`feishu`、`discord`、`telegram`、`slack`）に配信されます。チャネルと環境ごとに 1 件ずつ登録します：

```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 トンネリング** — localtunnel ではなく `cloudflared` を使用してください（落とし穴の表を参照）。

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