@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
npm install @waffo/pancake-ts
Quick Start
Most merchants create stores and products in the Dashboard. The SDK is primarily used for checkout integration — redirecting customers from your site to the Waffo checkout page.
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) |
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.
We recommend authenticated checkout whenever possible. The most important reason: authenticated checkout binds the order to the
buyerIdentityyou 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) 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 onlywithTrial— 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.
// 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.
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.
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}"
changeAmountvschangeCreditAmount— two ways to price the same change:changeAmountsets what you charge for this period,changeCreditAmountsets 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,changeCreditAmountandwithTrialare 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.anonymouscarries no plan change method at all. - Customer self-service — to let customers start a change themselves, switch on
selfServicePlanChangeon the product group (see Subscription Product Groups) and callcustomer.createPlanChangeSession()on their session (see 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
// 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.checkoutUrlreplaces the current page, preventing customers from returning to your site without browser back navigation.
See API Reference — 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)
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)
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 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:
// 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
buyerIdentityascheckout.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
selfServicePlanChangeis on (see 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.
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.
GraphQL — Typed Queries
// 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 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).
// 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. The following APIs are for merchants who need programmatic automation.
Stores
// 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.
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 alistmethod —client.graphql.queryis the only read path, by design.
Products
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).
// 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
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.
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:
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.
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 | Complete method reference — parameters, return types, BillingDetail fields |
| GraphQL Guide | Queries, filters, analytics, introspection, delivery logs |
| Webhook Guide | Signature verification, event types, event data fields, key resolution, retry mechanism |
| Changelog | 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 for the full list.
Development
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