推奨エントリーポイント
これは AI アシスト開発の主要入口ページです。AI コーディングエージェントが使用する最新の waffo-pancake スキル構造に対応しています:Waffo Pancake の適用場面、統合パスの選び方、商品モデリング、安全なリリース方法をカバーします。
AI コーディングアシスタントに伝えるだけ:
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 入口を開いてください。
完全なプロンプト
エージェントに完全な統合パスを計画・実装させたい場合は、このプロンプトを使用してください:
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 → Merchant → Integration (use this as `WAFFO_MERCHANT_ID`, not `storeId`)
2. Create an API Key from Dashboard → Merchant → Integration → 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.
Official Waffo Pancake Skill AI インテグレーションページ内の公式 Skill 入口です。開くと、チームが使っている標準 SKILL.md を表示、コピー、ダウンロードできます。
@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 〜 29〜 29 〜 199) SaaS(Starter/Pro/Enterprise) サブスクティア サブスク商品3個 + プラン切替用の商品グループ1個 デザインテンプレートショップ テンプレート単位の単発購入 テンプレートごとに単発商品1個、または priceSnapshot で価格を上書きする共有商品1個 API クレジットサービス クレジットパック + サブスク パックごとに単発商品1個 + 月間クォータ用サブスク
インストールとセットアップ
npm install @waffo/pancake-ts
サーバーサイド専用。Node.js 18+。依存関係ゼロ。
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 推奨):
cat private.pem | base64 | tr -d '\n'
const privateKey = Buffer . from ( process . env . WAFFO_PRIVATE_KEY_BASE64 ! , "base64" ). toString ( "utf-8" );
ファイルパス (ローカル開発):
import { readFileSync } from "fs" ;
const privateKey = readFileSync ( "./keys/private.pem" , "utf-8" );
クイックスタート:ゼロから作成
// 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 ({
storeId: store . id ,
productId: monthly . id ,
productType: "subscription" ,
currency: "USD" ,
});
// Redirect customer to session.checkoutUrl
Store ID と Product ID を環境変数に保存して今後使用します。
クイックスタート:既存商品を使用
ダッシュボードですでに商品を作成済みの場合、Product ID をコピーしてそのままチェックアウトへ:
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 リファレンス
ストア
// 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" });
単発商品
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
サブスクリプション商品
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
サブスクリプション商品グループ
グループは共有トライアルとサブスクリプション商品間のプラン切替を可能にします。
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" });
チェックアウトセッション
const session = await client . checkout . createSession ({
storeId: "store_id" , // optional if productId is from an existing store
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=ライト / 省略=ストアデフォルト
priceSnapshot は動的価格設定の核心パラメータです。 priceSnapshot を指定すると、商品に設定された価格は完全に無視されます。ユースケース:使用量ベースの階層価格設定、動的クーポン割引、A/Bテストによる異なる価格ポイントなど。
// 動的価格設定の例:使用量ティアに基づいて価格を上書き
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 ハンドラーパターン:
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 });
}
}
サブスクリプションのキャンセル
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! ではありません)。
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
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
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
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 );
}
});
検証オプション
// 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返金失敗
イベント構造
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 の設定
await client . stores . update ({
id: "store_id" ,
webhookSettings: {
testWebhookUrl: "https://your-domain.com/api/webhooks" ,
prodWebhookUrl: "https://your-domain.com/api/webhooks" ,
testEvents: [
"order.completed" ,
"subscription.activated" ,
"subscription.canceled" ,
"subscription.past_due" ,
],
prodEvents: [
"order.completed" ,
"subscription.activated" ,
"subscription.canceled" ,
"subscription.past_due" ,
],
},
});
エラーハンドリング
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] が最も外側の呼び出し元です。
開発のヒント
Webhook トンネリング — localtunnel ではなく cloudflared を使用してください(落とし穴の表を参照)。
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://localhost:3000
冪等性は自動 — SDK は merchantId + path + body から決定論的なキーを生成します。リトライは安全です。
Test → Prod ワークフロー — 商品はデフォルトで test 環境に作成されます。.publish() で本番環境にプロモートします。Webhook イベントには mode: "test" | "prod" が含まれるため、ハンドラーで区別できます。
クイックスタートチェックリスト
npm install @waffo/pancake-ts
WAFFO_MERCHANT_ID と WAFFO_PRIVATE_KEY 環境変数を設定
new WaffoPancake({ merchantId, privateKey }) でクライアントを初期化
ストアを作成または参照
商品を作成または参照
チェックアウトを作成:client.checkout.createSession(...) → checkoutUrl にリダイレクト
Webhook を処理:verifyWebhook(rawBody, sig) — 必ず request.text() を使用
Webhook URL を設定:client.stores.update({ webhookSettings: ... })