# Authentication Source: https://docs.waffo.ai/api-reference/authentication Secure your API requests with API Key authentication ## Authentication Overview Waffo Pancake supports two authentication methods for API access: | Method | Use Case | Description | | -------------- | ---------------------- | -------------------------------------------------------------- | | **API Key** | Server-to-server calls | Permanent authentication using RSA-SHA256 signatures | | **Store Slug** | Public checkout flows | Public access using `X-Store-Slug` and `X-Environment` headers | *** ## API Key Authentication API Keys provide permanent server-to-server authentication using RSA-SHA256 signatures. The private key never leaves your server. ### Request Headers ```bash theme={"system"} X-Merchant-Id: MER_2aUyqjCzEIiEcYMKj7TZtw X-Timestamp: 1705312200 X-Signature: BASE64_ENCODED_SIGNATURE Content-Type: application/json ``` **API Key authentication does not require the `X-Environment` header.** Each API Key is bound to either test or prod at creation time. The environment is determined by which key successfully verifies the signature. ### Using the SDK (Recommended) ```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!, }); // All requests are automatically signed const { store } = await client.stores.create({ name: "My Store" }); ``` ### Signing Algorithm (Manual Integration) If you're not using the SDK, you need to implement RSA-SHA256 request signing: ``` 1. Build the canonical request: canonicalRequest = METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + SHA256_BASE64(BODY) 2. Sign with RSA-SHA256: signature = RSA-SHA256(canonicalRequest, privateKey) 3. Base64 encode: X-Signature = Base64(signature) ``` ### Manual Signing Examples ```javascript Node.js theme={"system"} const crypto = require('crypto'); const MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw'; const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY----- ...your private key... -----END RSA PRIVATE KEY-----`; async function callApiWithSignature(method, path, body) { const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyStr = JSON.stringify(body); const bodyHash = crypto.createHash('sha256').update(bodyStr).digest('base64'); // Build canonical request const canonicalRequest = `${method}\n${path}\n${timestamp}\n${bodyHash}`; // RSA-SHA256 sign const signature = crypto.sign('sha256', Buffer.from(canonicalRequest), PRIVATE_KEY).toString('base64'); const response = await fetch(`https://api.waffo.ai${path}`, { method, headers: { 'Content-Type': 'application/json', 'X-Merchant-Id': MERCHANT_ID, 'X-Timestamp': timestamp, 'X-Signature': signature }, body: bodyStr }); return response.json(); } // Usage const result = await callApiWithSignature('POST', '/v1/actions/store/create-store', { name: 'My Store' }); ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"name":"My Store"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/create-store $TIMESTAMP $BODY_HASH" # Sign with openssl (requires private_key.pem file) SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/store/create-store" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```python Python theme={"system"} import hashlib import time import base64 import json import requests from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding MERCHANT_ID = 'MER_2aUyqjCzEIiEcYMKj7TZtw' PRIVATE_KEY = '''-----BEGIN RSA PRIVATE KEY----- ...your private key... -----END RSA PRIVATE KEY-----''' def call_api_with_signature(method, path, body): timestamp = str(int(time.time())) body_str = json.dumps(body, separators=(',', ':')) body_hash = base64.b64encode(hashlib.sha256(body_str.encode()).digest()).decode() # Build canonical request canonical_request = f"{method}\n{path}\n{timestamp}\n{body_hash}" # RSA-SHA256 sign private_key = serialization.load_pem_private_key(PRIVATE_KEY.encode(), password=None) signature = private_key.sign(canonical_request.encode(), padding.PKCS1v15(), hashes.SHA256()) signature_b64 = base64.b64encode(signature).decode() response = requests.post( f'https://api.waffo.ai{path}', headers={ 'Content-Type': 'application/json', 'X-Merchant-Id': MERCHANT_ID, 'X-Timestamp': timestamp, 'X-Signature': signature_b64 }, data=body_str ) return response.json() # Usage result = call_api_with_signature('POST', '/v1/actions/store/create-store', { 'name': 'My Store' }) ``` **Private Key Security** * Never expose the private key in client-side code * Do not commit private keys to version control * Store private keys in environment variables * Rotate keys regularly, especially after team changes * Timestamp must be within **5 minutes** of server time *** ## Store Slug Authentication For public-facing checkout flows, use Store Slug authentication. This allows visitors to create checkout sessions and query public store data without API Key credentials. ### Request Headers ```bash theme={"system"} X-Store-Slug: my-awesome-store-k8x2m9ab X-Environment: test | prod Content-Type: application/json ``` ### Example ```bash cURL theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "X-Store-Slug: my-awesome-store-k8x2m9ab" \ -H "X-Environment: test" \ -H "Content-Type: application/json" \ -d '{"productId": "PROD_4cWAslE1GKkGeaOMl9Vbmy", "productType": "onetime", "currency": "USD"}' ``` ```javascript JavaScript theme={"system"} const response = await fetch('https://api.waffo.ai/v1/graphql', { method: 'POST', headers: { 'X-Store-Slug': 'my-awesome-store-k8x2m9ab', 'Content-Type': 'application/json', 'X-Environment': 'prod' }, body: JSON.stringify({ query: `query { store { id name onetimeProducts { id name prices } } }` }) }); ``` *** ## Creating API Keys Navigate to Dashboard → Merchant → API & Development → API Keys Click "Create API Key" to generate a new RSA key pair. The public key is sent to the server automatically. Give it a descriptive name (e.g., "Production Server") and select the target environment (Test or Production). **Download your private key immediately.** It will not be shown again. Deleting an API key is immediate and irreversible. Any requests signed with the deleted key will fail with `401 Unauthorized`. *** ## Authentication Method Comparison | Feature | API Key | Store Slug | | ----------------------- | :------------------------: | :--------------------: | | Server-side use | Yes | No | | Client-side use | No | Yes | | Requires X-Environment | No | Yes | | Issue Session Token | Yes | No | | Create Checkout Session | Yes | Yes | | Product Management | Yes | No | | GraphQL Queries | Yes | Yes (public data only) | | Validity | Permanent (key-controlled) | - | *** ## Authentication Errors | Status | Error | Solution | | ------ | ------------------------ | ---------------------------------------------------------------------------- | | 401 | Invalid signature | Check signing algorithm, private key, and timestamp freshness (5 min window) | | 401 | Invalid or expired token | Re-authenticate or use a valid API Key | | 403 | Insufficient permissions | Check if the role has access to the endpoint | | 400 | Missing authentication | Ensure required headers are present | ```json theme={"system"} { "data": null, "errors": [ { "message": "Invalid signature", "layer": "gateway" } ] } ``` *** ## Security Best Practices 1. **Never expose private keys** in client-side code, version control, or public repositories 2. **Use HTTPS** for all API requests 3. **Verify Webhook signatures** to prevent forged requests 4. **Separate test and production keys** -- create distinct keys for each environment 5. **Rotate keys regularly**, especially after team member changes 6. **Monitor API usage** in the Dashboard for unusual activity # Customer Portal API Overview Source: https://docs.waffo.ai/api-reference/endpoints/auth/customer-endpoints Endpoints the customer's browser calls with a session token minted via Issue Session Token The Customer Portal API is the buyer-side surface of Waffo. The merchant's backend mints a short-lived session token via [Issue Session Token](/api-reference/endpoints/auth/issue-session-token), and the customer's browser (or your in-product UI) calls the endpoints below with that token in the `Authorization` header. The merchant API Key never leaves the server. Session tokens are scoped by `buyerIdentity` + `visitingStoreId` and only authorize operations on records that belong to that customer within that store. They cannot read merchant-wide state. ## Required headers on every call | Header | Value | | --------------- | ---------------------------------------------------------------------- | | `Authorization` | `Bearer ` (the JWT returned by Issue Session Token) | | `X-Environment` | `test` or `prod` (must match the environment the token was minted for) | The gateway derives the buyer identity and visiting store from the token; you do not pass them as separate headers. ## Endpoints Check whether the customer is eligible for a trial period Create a one-time order for the authenticated customer Cancel a pending one-time order before payment completes Subscribe the authenticated customer to a recurring plan Cancel an active or pending subscription on behalf of the customer Reverse a pending cancellation while still in `canceling` Switch the active subscription to a different product Query the customer's own orders, payments, and refunds ## Token-path errors These errors are surfaced only on the session-token call path: | Status | `errors[0].message` | What it means | | ------ | -------------------------------------------- | -------------------------------------------------------------------- | | 400 | `Missing X-Context-Buyer-Identity header` | Token did not carry a buyer identity claim — re-mint the token | | 400 | `Missing X-Context-Visiting-Store-Id header` | Token was not scoped to a store — re-mint with `visitingStoreId` | | 401 | `Authentication failed` | Session token expired, malformed, or signed for a different merchant | # Issue Session Token Source: https://docs.waffo.ai/api-reference/endpoints/auth/issue-session-token Issue a session token for a consumer to create orders in your store Issue a Session Token for a consumer to create orders in your store. This is an **API Key exclusive** endpoint. ``` POST /v1/actions/auth/issue-session-token ``` **Authentication:** API Key ## How It Works 1. Your server calls this endpoint with API Key authentication 2. You receive a short-lived session token 3. Pass the token to the consumer's browser 4. The consumer uses the token (`Authorization: Bearer `) to create orders and interact with checkout Session tokens are scoped to a single store and expire automatically. ## Request Body | Field | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `storeId` | string | No | Target store ID (Short ID format `STO_xxx`). Required when `productId` is not provided | | `productId` | string | No | Product ID (Short ID format `PROD_xxx`). When provided without `storeId`, the server derives the store from the product | | `buyerIdentity` | string | Yes | Consumer identity for order attribution (e.g., email or internal user ID). Encoded into the session JWT | ## Example Request ```typescript SDK theme={"system"} // Using storeId const { token, expiresAt } = await client.auth.issueSessionToken({ storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw", buyerIdentity: "customer@example.com", }); // Using productId (storeId derived automatically) const { token, expiresAt } = await client.auth.issueSessionToken({ productId: "PROD_7J3K5L8M2N4P6Q9R", buyerIdentity: "customer@example.com", }); ``` ```bash cURL (with storeId) theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"storeId":"STO_2aUyqjCzEIiEcYMKj7TZtw","buyerIdentity":"customer@example.com"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/auth/issue-session-token $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/auth/issue-session-token" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash cURL (with productId) theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"productId":"PROD_7J3K5L8M2N4P6Q9R","buyerIdentity":"customer@example.com"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/auth/issue-session-token $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/auth/issue-session-token" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```python Python theme={"system"} # Using storeId result = call_api_with_signature('POST', '/v1/actions/auth/issue-session-token', { 'storeId': 'STO_2aUyqjCzEIiEcYMKj7TZtw', 'buyerIdentity': 'customer@example.com' }) # Using productId result = call_api_with_signature('POST', '/v1/actions/auth/issue-session-token', { 'productId': 'PROD_7J3K5L8M2N4P6Q9R', 'buyerIdentity': 'customer@example.com' }) print(result['data']['token']) ``` ## Success Response (200) ```json theme={"system"} { "data": { "token": "opaque-session-token...", "expiresAt": "2024-01-15T11:00:00.000Z" } } ``` ## Response Fields | Field | Type | Description | | ----------- | ------ | -------------------------- | | `token` | string | Session Token | | `expiresAt` | string | Expiration time (ISO 8601) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: buyerIdentity` | `buyerIdentity` is empty or missing | Fix the request body, then resubmit | | 400 | `Missing required field: provide storeId or productId` | Neither `storeId` nor `productId` was provided | Provide one of them, then resubmit | | 400 | `Expected format: STO_xxx, got "..."` | `storeId` Short ID could not be decoded | Fix the `storeId` format, then resubmit | | 400 | `Expected format: PROD_xxx, got "..."` | `productId` Short ID could not be decoded | Fix the `productId` format, then resubmit | | 400 | `Store is not active` | Store exists but its status is not `active` | Activate the store, then resubmit | | 401 | `Missing merchantId in request context` | API Key authentication did not resolve a merchant | Verify API Key headers and signature | | 403 | `Access denied: you do not have permission to this store` | Merchant does not own the store | Verify store ownership | | 404 | `Store not found` | Store does not exist or has been deleted | Verify the store ID | | 404 | `Product not found` | Product does not exist | Verify the product ID | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Auth Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/auth/overview Issue session tokens for checkout flows Auth endpoints are **API Key exclusive**. Use these endpoints to issue session tokens that allow consumers to interact with your store's checkout flow. ## Typical Flow: Embedded Checkout ```mermaid theme={"system"} sequenceDiagram participant C as Consumer Browser participant S as Your Server participant W as Waffo Pancake API C->>S: Click "Buy Now" S->>W: POST /issue-session-token Note over S,W: API Key signature W->>S: { token, expiresAt } S->>C: Return token + checkout URL C->>W: POST /create-order Note over C,W: Bearer sessionToken W->>C: { checkoutUrl } C->>C: Redirect to checkout page ``` ## Endpoints Issue a session token for a consumer to create orders in your store. # GraphQL API Source: https://docs.waffo.ai/api-reference/endpoints/graphql/overview Query all your data with GraphQL read-only queries ## Overview The Waffo Pancake GraphQL API provides **read-only** access to all your data. Use REST action endpoints for writes, and GraphQL for queries. ``` POST /v1/graphql ``` **Authentication:** API Key The GraphQL API supports **queries only**. All write operations (create, update, delete) use the [REST action endpoints](/api-reference/introduction#endpoint-groups). ## Making a Request ### Request Body | Field | Type | Required | Description | | ----------- | ------ | -------- | -------------------- | | `query` | string | Yes | GraphQL query string | | `variables` | object | No | Query variables | ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); interface StoresQuery { stores: Array<{ id: string; name: string; status: string }>; } const result = await client.graphql.query({ query: `{ stores { id name status } }`, }); console.log(result.stores); // => [{ id: "STO_2aUyqjCzEIiEcYMKj7TZtw", name: "My Store", status: "active" }] ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/graphql", { query: "query { stores { id name status } }", variables: {}, }); console.log(result.data.stores); // => [{ id: "STO_2aUyqjCzEIiEcYMKj7TZtw", name: "My Store", status: "active" }] ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"query":"query { stores { id name status } }","variables":{}}"""; String response = callApi("POST", "/v1/graphql", body); System.out.println(response); // => {"data":{"stores":[{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"My Store","status":"active"}]}} ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/graphql", { "query": "query { stores { id name status } }", "variables": {}, }) stores = result["data"]["stores"] print(stores) # => [{"id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "My Store", "status": "active"}] ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "query": "query { stores { id name status } }", "variables": map[string]interface{}{}, } result, err := callAPI("POST", "/v1/graphql", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) // => {"data":{"stores":[{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"My Store","status":"active"}]}} ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "query": "query { stores { id name status } }", "variables": {} }); let result = call_api("POST", "/v1/graphql", &body).await?; println!("{}", result); // => {"data":{"stores":[{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"My Store","status":"active"}]}} ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"query\":\"query { stores { id name status } }\",\"variables\":{}}"; char *response = call_api("POST", "/v1/graphql", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"query":"query { stores { id name status } }","variables":{}})"; std::string response = callApi("POST", "/v1/graphql", body); std::cout << response << std::endl; // => {"data":{"stores":[{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"My Store","status":"active"}]}} ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"query":"query { stores { id name status } }","variables":{}}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/graphql $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/graphql" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"query":"query { stores { id name status } }","variables":{}}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/graphql $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/graphql" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ### Status Codes | Status | Description | | ------ | ------------------------------------------------------------------------------ | | 200 | Query succeeded (GraphQL errors in response body `errors` field) | | 400 | Missing query / depth exceeded / invalid environment / missing or invalid role | | 401 | Authentication failed | | 403 | Operation type not allowed (only Query supported) | | 500 | Internal server error | *** ## Schema Introspection **Recommended:** Use standard GraphQL introspection to discover the full schema. This ensures you always work with the latest types and fields. **GraphQL types differ from REST/SDK types.** For example, `prices` is a `Record` in REST but `[CurrencyPrice!]!` (array of `{currency, priceInfo}`) in GraphQL, and `metadata` is a parsed object in REST but a JSON string in GraphQL. Do not use SDK TypeScript type definitions to construct GraphQL queries — always use introspection or the examples below. ### Discover All Available Queries ```graphql theme={"system"} query { __schema { queryType { fields { name description args { name type { name kind } } } } } } ``` ### Discover Type Fields ```graphql theme={"system"} query { __type(name: "Store") { name fields { name type { name kind ofType { name } } } } } ``` API Key authentication gives you access to **18 query types** including single-entity queries, list queries with filters, count queries, and analytics queries. *** ## Filtering GraphQL queries support filters using typed filter objects: | Type | Operators | Example | | ---------------- | ------------------------------ | -------------------------------------- | | `StringFilter` | `eq`, `ne`, `in`, `contains` | `{ storeId: { eq: "STO_xxx" } }` | | `DateTimeFilter` | `eq`, `gt`, `lt`, `gte`, `lte` | `{ createdAt: { gte: "2026-01-01" } }` | | `IntFilter` | `eq`, `gt`, `lt`, `gte`, `lte` | `{ amount: { gte: 1000 } }` | | `BooleanFilter` | `eq` | `{ isActive: { eq: true } }` | ```graphql theme={"system"} query { onetimeProducts( filter: { storeId: { eq: "STO_3bVzrkD0FJjFdZNLk8Ualx" } status: { eq: "active" } } ) { id name prices } } ``` *** ## Pagination Use `limit` and `offset` for pagination. Use `*Count` queries to get total counts. ```graphql theme={"system"} query { onetimeProducts( filter: { storeId: { eq: "STO_3bVzrkD0FJjFdZNLk8Ualx" } } limit: 10 offset: 0 ) { id name } onetimeProductsCount( filter: { storeId: { eq: "STO_3bVzrkD0FJjFdZNLk8Ualx" } } ) } ``` | Parameter | Type | Description | | --------- | ------- | ----------------------------------- | | `limit` | integer | Maximum number of results to return | | `offset` | integer | Number of results to skip | *** ## Environment-Specific Fields The API Key environment affects which product data is returned: * `version` -- Returns the version for the specified environment * `status` -- Returns the status in the specified environment A product may be `active` in test but `inactive` in production if it hasn't been published yet. *** ## Query Examples Query stores, one-time products, subscription products, and product versions Query orders, subscription orders, payments, and refund tickets Revenue statistics, payment analytics, trend analysis, and customer insights # Create Product Source: https://docs.waffo.ai/api-reference/endpoints/onetime-products/create-product Create a new one-time purchase product with multi-currency pricing Create a new one-time purchase product with multi-currency pricing. ``` POST /v1/actions/onetime-product/create-product ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `storeId` | string | Yes | Store ID (Short ID format `STO_xxx`) | | `name` | string | Yes | Product name (max 64 chars) | | `description` | string \| null | No | Product description (supports Markdown; pass `null` or `""` to clear) | | `prices` | object | Yes | Multi-currency pricing map (see below) | | `media` | array | No | Product images/videos (see below) | | `successUrl` | string \| null | No | Redirect URL after successful purchase (max 512 chars, must be a valid http(s) URL; pass `null` or `""` to clear) | | `metadata` | object | No | Custom key-value data (max 50 keys) | ## Price Object Format Prices are a map of ISO 4217 currency codes to price configuration objects. At least one currency is required. ```json theme={"system"} { "USD": { "amount": "29.00", "taxIncluded": false, "taxCategory": "saas" }, "EUR": { "amount": "27.00", "taxIncluded": true, "taxCategory": "saas" } } ``` | Field | Type | Description | | ------------- | ------- | ---------------------------------------------------------- | | `amount` | string | Price as a display format string (e.g., "29.00" = \$29.00) | | `taxIncluded` | boolean | Whether the amount already includes tax | | `taxCategory` | string | Tax category for tax calculation | **Supported `taxCategory` values:** | Value | Description | | ---------------------- | ---------------------------- | | `digital_goods` | General digital goods | | `saas` | Software as a Service | | `software` | Downloadable software | | `ebook` | Electronic books | | `online_course` | Online courses and education | | `consulting` | Consulting services | | `professional_service` | Professional services | ## Media Item Format ```json theme={"system"} { "type": "image", "url": "https://example.com/product.png", "alt": "Product screenshot", "thumbnail": "https://example.com/product-thumb.png" } ``` | Field | Type | Description | | ----------- | ------ | ----------------------------------------------------------- | | `type` | string | `image` or `video` | | `url` | string | Media URL | | `alt` | string | Alt text for accessibility | | `thumbnail` | string | Thumbnail URL (optional for images, recommended for videos) | ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake, TaxCategory } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { product } = await client.onetimeProducts.create({ storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw", name: "Premium Template Pack", description: "50 premium design templates for your next project.", prices: { USD: { amount: "49.00", taxIncluded: false, taxCategory: TaxCategory.DigitalGoods }, EUR: { amount: "45.00", taxIncluded: true, taxCategory: TaxCategory.DigitalGoods }, }, media: [ { type: "image", url: "https://example.com/templates-preview.png", alt: "Template preview" }, ], successUrl: "https://example.com/thank-you", metadata: { category: "design", fileCount: "50" }, }); ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack", "description": "50 premium design templates for your next project.", "prices": { "USD": { "amount": "49.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "45.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/create-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/create-product" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```python Python theme={"system"} import hashlib, time, base64, json, requests from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding merchant_id = "MER_2aUyqjCzEIiEcYMKj7TZtw" timestamp = str(int(time.time())) body = { "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack", "description": "50 premium design templates for your next project.", "prices": { "USD": {"amount": "49.00", "taxIncluded": False, "taxCategory": "digital_goods"}, "EUR": {"amount": "45.00", "taxIncluded": True, "taxCategory": "digital_goods"}, }, "successUrl": "https://example.com/thank-you", } body_json = json.dumps(body, separators=(",", ":")) body_hash = base64.b64encode(hashlib.sha256(body_json.encode()).digest()).decode() path = "/v1/actions/onetime-product/create-product" canonical = f"POST\n{path}\n{timestamp}\n{body_hash}" with open("private_key.pem", "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=None) signature = base64.b64encode( private_key.sign(canonical.encode(), padding.PKCS1v15(), hashes.SHA256()) ).decode() resp = requests.post( f"https://api.waffo.ai{path}", headers={ "Content-Type": "application/json", "X-Merchant-Id": merchant_id, "X-Timestamp": timestamp, "X-Signature": signature, }, data=body_json, ) print(resp.json()) ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack", "description": "50 premium design templates for your next project.", "prices": { "USD": { "amount": "49.00", "taxCategory": "digital_goods" }, "EUR": { "amount": "45.00", "taxCategory": "digital_goods" } }, "media": [ { "type": "image", "url": "https://example.com/templates-preview.png", "alt": "Template preview" } ], "successUrl": "https://example.com/thank-you", "metadata": { "category": "design", "fileCount": "50" }, "status": "active", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T10:30:00.000Z" } } } ``` ## Response Fields The response is wrapped in `data.product`. The product object is a flattened detail view combining product and version fields. | Field | Type | Description | | ------------- | -------------- | --------------------------------------------------------- | | `id` | string | Product ID (`PROD_xxx`) | | `storeId` | string | Store ID (`STO_xxx`) | | `name` | string | Product name (from current version) | | `description` | string \| null | Product description (from current version) | | `prices` | object | Multi-currency pricing map (see below) | | `media` | array | Media items (from current version) | | `successUrl` | string \| null | Success redirect URL (from current version) | | `metadata` | object | Custom metadata (from current version) | | `status` | string | Status in the current environment: `active` or `inactive` | | `createdAt` | string | Product creation timestamp (ISO 8601) | | `updatedAt` | string | Product last update timestamp (ISO 8601) | **Response price object** (per currency): | Field | Type | Description | | ------------- | ------ | ------------------------------------------------ | | `amount` | string | Price as a display format string (e.g., "49.00") | | `taxCategory` | string | Tax category for tax calculation | The `status` field reflects the environment you are operating in. When created in the **test** environment (default), status is `active`. When created in the **production** environment, the production status is returned. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ---------------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: storeId` | `storeId` not provided | Fix the body, resubmit | | 400 | `Invalid ID format` | The provided Short ID could not be decoded | Fix the `storeId` format, resubmit | | 400 | `Missing required field: name` | `name` not provided | Fix the body, resubmit | | 400 | `Prices must have at least one currency` | `prices` is missing or an empty object | Provide at least one currency entry, resubmit | | 400 | `Invalid currency code` | Currency code must be exactly 3 uppercase letters (ISO 4217, e.g. `USD`, `EUR`, `JPY`) | Use a valid ISO 4217 code, resubmit | | 400 | `Invalid amount` | Amount must be a positive number string (e.g. `"9.99"`, `"100"`) | Use a positive number string, resubmit | | 404 | `Store not found` | Store does not exist or is not accessible | Verify the `storeId` belongs to your merchant account | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # One-Time Product Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/onetime-products/overview Create and manage single-purchase products One-time products represent single-purchase digital goods. Each product supports multi-currency pricing, immutable versioning, and separate test/production environments. ## Product Status Values | Status | Description | | ---------- | ----------------------------------------------------------- | | `active` | Product is live and purchasable via checkout | | `inactive` | Product is hidden from checkout, existing orders unaffected | ## Typical Workflow ```mermaid theme={"system"} sequenceDiagram participant M as Merchant participant T as Test Environment participant P as Production M->>T: Create Product (test) T-->>M: testStatus=active, prodStatus=inactive M->>T: Update Product (iterate) T-->>M: New version created (v2, v3...) M->>T: Publish Product T->>P: Copy test version to production P-->>M: prodStatus=active, testStatus=active M->>P: Update Product (X-Environment: prod) P-->>M: New production version M->>P: Update Status (inactive) P-->>M: Product hidden from checkout ``` ## Endpoints Create a new one-time purchase product with multi-currency pricing. Update product content. A new immutable version is created if content changes. Publish a product from the test environment to production. Activate or deactivate a product for checkout visibility. # Publish Product Source: https://docs.waffo.ai/api-reference/endpoints/onetime-products/publish-product Publish a one-time product from the test environment to production Publish a product from the test environment to production. This is a **one-way, first-publish-only** operation that copies the current test version to production. ``` POST /v1/actions/onetime-product/publish-product ``` **Authentication:** API Key Do **not** include the `X-Environment` header for this endpoint. Publishing is always one-way from test to production. ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | --------------------------------------- | | `id` | string | Yes | Product ID (Short ID format `PROD_xxx`) | ## Example Request ```typescript SDK theme={"system"} const { product } = await client.onetimeProducts.publish({ id: "PROD_3kF9mNpQrStUvWxYz1A2bC", }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/onetime-product/publish-product", { id: "PROD_3kF9mNpQrStUvWxYz1A2bC", }); console.log(result.data.product.status); // => "active" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"PROD_3kF9mNpQrStUvWxYz1A2bC"}"""; String response = callApi("POST", "/v1/actions/onetime-product/publish-product", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/onetime-product/publish-product", { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", }) print(result["data"]["product"]["status"]) # => "active" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", } result, err := callAPI("POST", "/v1/actions/onetime-product/publish-product", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC" }); let result = call_api("POST", "/v1/actions/onetime-product/publish-product", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3kF9mNpQrStUvWxYz1A2bC\"}"; char *response = call_api("POST", "/v1/actions/onetime-product/publish-product", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"PROD_3kF9mNpQrStUvWxYz1A2bC"})"; std::string response = callApi("POST", "/v1/actions/onetime-product/publish-product", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3kF9mNpQrStUvWxYz1A2bC"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/publish-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/publish-product" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3kF9mNpQrStUvWxYz1A2bC"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/publish-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/onetime-product/publish-product" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack", "description": "50 premium design templates for your next project.", "prices": { "USD": { "amount": "49.00", "taxCategory": "digital_goods" }, "EUR": { "amount": "45.00", "taxCategory": "digital_goods" } }, "media": [ { "type": "image", "url": "https://example.com/templates-preview.png", "alt": "Template preview" } ], "successUrl": "https://example.com/thank-you", "metadata": { "category": "design", "fileCount": "50" }, "status": "active", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T12:00:00.000Z" } } } ``` ## Response Fields Same as [Create Product response](/api-reference/endpoints/onetime-products/create-product#response-fields). Only the **first publish** is supported. Once a product has a production version, this endpoint cannot be used again for the same product. To update the production version after initial publish, use the [Update Product](/api-reference/endpoints/onetime-products/update-product) endpoint with the `X-Environment: prod` header. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | 400 | `Missing required field: id` | `id` not provided | Fix the body, resubmit | | 400 | `Invalid ID format` | The provided Short ID could not be decoded | Fix the `id` format, resubmit | | 400 | `No test version found` | The product has no version in the test environment yet | Create the product first via [Create Product](/api-reference/endpoints/onetime-products/create-product) | | 400 | `Test version is not active` | The current test version's status is `inactive` | Activate the test version via [Update Status](/api-reference/endpoints/onetime-products/update-status) first | | 400 | `Already published to production` | The product already has a production version | Use [Update Product](/api-reference/endpoints/onetime-products/update-product) with `X-Environment: prod` to make further changes | | 404 | `Product not found` | Product does not exist or is not accessible | Verify the `id` belongs to your merchant account | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Product Source: https://docs.waffo.ai/api-reference/endpoints/onetime-products/update-product Update a one-time product's content with automatic immutable versioning Update a one-time product's content. If the content has changed, a new immutable version is created automatically. If the content is identical to the current version, no new version is created. ``` POST /v1/actions/onetime-product/update-product ``` **Authentication:** API Key Use the [Update Status](/api-reference/endpoints/onetime-products/update-status) endpoint to change a product's status (`active`/`inactive`). This endpoint is for content updates only. ## Request Body | Field | Type | Required | Description | | ------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------- | | `id` | string | Yes | Product ID (Short ID format `PROD_xxx`) | | `name` | string | No | Updated product name (max 64 chars) | | `description` | string \| null | No | Updated description (pass `null` or `""` to clear) | | `prices` | object | No | Updated multi-currency pricing map | | `media` | array | No | Updated media items | | `successUrl` | string \| null | No | Updated redirect URL (max 512 chars, must be a valid http(s) URL; pass `null` or `""` to clear) | | `metadata` | object | No | Updated custom metadata | For the `prices` and `media` object formats, see [Create Product](/api-reference/endpoints/onetime-products/create-product#price-object-format). ## Example Request ```typescript SDK theme={"system"} const { product } = await client.onetimeProducts.update({ id: "PROD_3kF9mNpQrStUvWxYz1A2bC", name: "Premium Template Pack v2", description: "75 premium design templates — expanded collection.", prices: { USD: { amount: "59.00", taxIncluded: false, taxCategory: TaxCategory.DigitalGoods }, EUR: { amount: "55.00", taxIncluded: true, taxCategory: TaxCategory.DigitalGoods }, }, successUrl: "https://example.com/thank-you", }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/onetime-product/update-product", { id: "PROD_3kF9mNpQrStUvWxYz1A2bC", name: "Premium Template Pack v2", description: "75 premium design templates — expanded collection.", prices: { USD: { amount: "59.00", taxIncluded: false, taxCategory: "digital_goods" }, EUR: { amount: "55.00", taxIncluded: true, taxCategory: "digital_goods" }, }, successUrl: "https://example.com/thank-you", }); console.log(result.data.product.name); // => "Premium Template Pack v2" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" }"""; String response = callApi("POST", "/v1/actions/onetime-product/update-product", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/onetime-product/update-product", { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": {"amount": "59.00", "taxIncluded": False, "taxCategory": "digital_goods"}, "EUR": {"amount": "55.00", "taxIncluded": True, "taxCategory": "digital_goods"}, }, "successUrl": "https://example.com/thank-you", }) print(result["data"]["product"]["name"]) # => "Premium Template Pack v2" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": map[string]interface{}{ "USD": map[string]interface{}{"amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods"}, "EUR": map[string]interface{}{"amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods"}, }, "successUrl": "https://example.com/thank-you", } result, err := callAPI("POST", "/v1/actions/onetime-product/update-product", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" }); let result = call_api("POST", "/v1/actions/onetime-product/update-product", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3kF9mNpQrStUvWxYz1A2bC\"," "\"name\":\"Premium Template Pack v2\"," "\"description\":\"75 premium design templates — expanded collection.\"," "\"prices\":{\"USD\":{\"amount\":\"59.00\",\"taxIncluded\":false,\"taxCategory\":\"digital_goods\"}," "\"EUR\":{\"amount\":\"55.00\",\"taxIncluded\":true,\"taxCategory\":\"digital_goods\"}}," "\"successUrl\":\"https://example.com/thank-you\"}"; char *response = call_api("POST", "/v1/actions/onetime-product/update-product", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" })"; std::string response = callApi("POST", "/v1/actions/onetime-product/update-product", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/update-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/update-product" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxIncluded": false, "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxIncluded": true, "taxCategory": "digital_goods" } }, "successUrl": "https://example.com/thank-you" }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/update-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/onetime-product/update-product" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack v2", "description": "75 premium design templates — expanded collection.", "prices": { "USD": { "amount": "59.00", "taxCategory": "digital_goods" }, "EUR": { "amount": "55.00", "taxCategory": "digital_goods" } }, "media": [], "successUrl": "https://example.com/thank-you", "metadata": {}, "status": "active", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T11:00:00.000Z" } } } ``` ## Response Fields Same as [Create Product response](/api-reference/endpoints/onetime-products/create-product#response-fields). Product versions are **immutable**. Existing orders retain their original version. New purchases always use the latest version. If the submitted content is identical to the current version, no new version is created and the existing version is returned. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 400 | `Missing required field: id` | `id` not provided | Fix the body, resubmit | | 400 | `Invalid ID format` | The provided Short ID could not be decoded | Fix the `id` format, resubmit | | 400 | `Invalid currency code` | Currency code must be exactly 3 uppercase letters (ISO 4217, e.g. `USD`, `EUR`, `JPY`) | Use a valid ISO 4217 code, resubmit | | 400 | `Invalid amount` | Amount must be a positive number string (e.g. `"9.99"`, `"100"`) | Use a positive number string, resubmit | | 400 | `Product X has no version in environment Y` | The product has no version in the targeted environment (e.g. attempting to update `prod` before publishing) | Publish the product to the target environment first via [Publish Product](/api-reference/endpoints/onetime-products/publish-product) | | 404 | `Product not found` | Product does not exist or is not accessible | Verify the `id` belongs to your merchant account | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Status Source: https://docs.waffo.ai/api-reference/endpoints/onetime-products/update-status Activate or deactivate a one-time product for checkout visibility Activate or deactivate a product. Active products are purchasable; inactive products are hidden from checkout. ``` POST /v1/actions/onetime-product/update-status ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | -------- | ------ | -------- | --------------------------------------- | | `id` | string | Yes | Product ID (Short ID format `PROD_xxx`) | | `status` | string | Yes | `active` or `inactive` | ## Example Request ```typescript SDK theme={"system"} import { ProductVersionStatus } from "@waffo/pancake-ts"; const { product } = await client.onetimeProducts.updateStatus({ id: "PROD_3kF9mNpQrStUvWxYz1A2bC", status: ProductVersionStatus.Inactive, }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/onetime-product/update-status", { id: "PROD_3kF9mNpQrStUvWxYz1A2bC", status: "inactive", }); console.log(result.data.product.status); // => "inactive" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"PROD_3kF9mNpQrStUvWxYz1A2bC","status":"inactive"}"""; String response = callApi("POST", "/v1/actions/onetime-product/update-status", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/onetime-product/update-status", { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "status": "inactive", }) print(result["data"]["product"]["status"]) # => "inactive" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "status": "inactive", } result, err := callAPI("POST", "/v1/actions/onetime-product/update-status", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "status": "inactive" }); let result = call_api("POST", "/v1/actions/onetime-product/update-status", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3kF9mNpQrStUvWxYz1A2bC\",\"status\":\"inactive\"}"; char *response = call_api("POST", "/v1/actions/onetime-product/update-status", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"PROD_3kF9mNpQrStUvWxYz1A2bC","status":"inactive"})"; std::string response = callApi("POST", "/v1/actions/onetime-product/update-status", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3kF9mNpQrStUvWxYz1A2bC","status":"inactive"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/update-status $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/onetime-product/update-status" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3kF9mNpQrStUvWxYz1A2bC","status":"inactive"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/onetime-product/update-status $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/onetime-product/update-status" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3kF9mNpQrStUvWxYz1A2bC", "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Premium Template Pack", "description": "50 premium design templates for your next project.", "prices": { "USD": { "amount": "49.00", "taxCategory": "digital_goods" }, "EUR": { "amount": "45.00", "taxCategory": "digital_goods" } }, "media": [ { "type": "image", "url": "https://example.com/templates-preview.png", "alt": "Template preview" } ], "successUrl": "https://example.com/thank-you", "metadata": { "category": "design", "fileCount": "50" }, "status": "inactive", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T13:00:00.000Z" } } } ``` ## Response Fields Same as [Create Product response](/api-reference/endpoints/onetime-products/create-product#response-fields). Setting a product to `inactive` hides it from checkout but does **not** affect existing orders. Customers who already purchased the product retain access. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 400 | `Missing required field: id` | `id` not provided | Fix the body, resubmit | | 400 | `Invalid ID format` | The provided Short ID could not be decoded | Fix the `id` format, resubmit | | 400 | `Invalid or missing status (must be 'active' or 'inactive')` | `status` is not `active` or `inactive` | Use `active` or `inactive`, resubmit | | 400 | `Product X has no version in environment Y` | The product has no version in the targeted environment (e.g. attempting to update status of `prod` before publishing) | Publish the product to the target environment first via [Publish Product](/api-reference/endpoints/onetime-products/publish-product) | | 404 | `Product not found` | Product does not exist or is not accessible | Verify the `id` belongs to your merchant account | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Cancel One-Time Order Source: https://docs.waffo.ai/api-reference/endpoints/orders/cancel-onetime-order Cancel a pending one-time order before payment is completed Cancel a pending one-time order. Only orders in `pending` status can be canceled. ``` POST /v1/actions/onetime-order/cancel-order ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer or buyer role) ## Cancellation Behavior | Current Status | Action | Result Status | | -------------- | ---------------- | -------------------- | | `pending` | Immediate cancel | `canceled` | | `completed` | Rejected | unchanged (terminal) | | `canceled` | Rejected | unchanged (terminal) | * **pending**: The order is canceled immediately and its status becomes `canceled` * The underlying PSP checkout session expires automatically — there is no separate PSP cancel call to make * `completed` and `canceled` are terminal states and cannot be canceled again ## Request Body | Field | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------------- | | `orderId` | string | Yes | One-time order ID (Short ID format `ORD_xxx`) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.cancelOnetime({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(result.orderId); // "ORD_2aUyqjCzEIiEcYMKj7TZtw" console.log(result.status); // "canceled" ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/onetime-order/cancel-order", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/onetime-order/cancel-order" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' \ "https://api.waffo.ai/v1/actions/onetime-order/cancel-order" ``` ## Success Response (200) ```json theme={"system"} { "data": { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", "status": "canceled" } } ``` ### Response Fields | Field | Type | Description | | --------- | ------ | ----------------------------------------------- | | `orderId` | string | Order ID (Short ID) | | `status` | string | New order status (always `canceled` on success) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: orderId` | `orderId` was not provided in the body | Fix the request body, then resubmit | | 400 | `Expected format: ORD_xxx, got "..."` | `orderId` Short ID could not be decoded | Fix the `orderId` format, then resubmit | | 400 | `Order cannot be canceled, current status: X` | Order status is not `pending` (e.g. already `completed` or `canceled`) | The order is no longer cancellable | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Order does not belong to user` | Ownership check failed | Verify the caller owns the order | | 404 | `Order not found` | Order does not exist | Verify the order ID | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Create Checkout Session Source: https://docs.waffo.ai/api-reference/endpoints/orders/create-checkout-session Lock product version, pricing, and currency for checkout Create a checkout session that locks product version, pricing, and currency. This is the first step in the checkout flow for both one-time and subscription products. The typical flow is: 1. Merchant creates a checkout session (server-side) 2. Returns `checkoutUrl` to the frontend 3. Consumer clicks the link and lands on the hosted checkout page 4. Consumer fills in billing details, previews tax, and completes the order ``` POST /v1/actions/checkout/create-session ``` **Authentication:** API Key or Store Slug ## Request Body (API Key) | Field | Type | Required | Description | | ------------------------- | ------- | -------- | ------------------------------------------------------------------- | | `productId` | string | Yes | Product ID in Short ID format (`PROD_xxx`) | | `currency` | string | Yes | ISO 4217 currency code (e.g. `USD`, `EUR`, `JPY`) | | `priceSnapshot` | object | No | Override pricing at session creation (API Key only, see below) | | `withTrial` | boolean | No | Enable trial period (subscription products only) | | `buyerEmail` | string | No | Pre-fill consumer's email on the checkout page | | `billingDetail` | object | No | Pre-fill billing details (see below) | | `successUrl` | string | No | Override redirect URL after successful payment | | `expiresInSeconds` | number | No | Session TTL in seconds (default: `2700` = 45 minutes, API Key only) | | `darkMode` | boolean | No | Enable dark mode for the checkout page | | `metadata` | object | No | Custom key-value pairs attached to the session | | `orderMerchantExternalId` | string | No | Your business-side order reference (max 128 chars). | ## Request Body (Store Slug) | Field | Type | Required | Description | | --------------- | ------- | -------- | ---------------------------------------------- | | `productId` | string | Yes | Product ID in Short ID format (`PROD_xxx`) | | `currency` | string | Yes | ISO 4217 currency code | | `buyerEmail` | string | No | Pre-fill consumer's email | | `billingDetail` | object | No | Pre-fill billing details (see below) | | `successUrl` | string | No | Override redirect URL after successful payment | | `darkMode` | boolean | No | Enable dark mode for the checkout page | Store Slug authentication does not support `priceSnapshot`, `expiresInSeconds`, `metadata`, `orderMerchantExternalId`, or `withTrial`. These fields are silently ignored to prevent price tampering and session manipulation from the client side. **ID-naming convention**: the same flat dual-key names (`orderMerchantExternalId` on checkout-side, `refundTicketMerchantExternalId` on refund-ticket-side) are used across request bodies, webhook payloads, and every GraphQL type that carries the value — so a value written at checkout can be read back from `Order`, `Payment`, `Refund`, or a webhook payload under the same field name. ## Price Snapshot Object | Field | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------------------------- | | `amount` | string | Yes | Price as a display format string (e.g., "29.00" = \$29.00) | | `taxIncluded` | boolean | Yes | Whether the amount already includes tax | | `taxCategory` | string | Yes | Tax category for tax calculation (e.g., `saas`, `digital_goods`) | ## Billing Detail Object | Field | Type | Required | Description | | -------------- | ------- | ----------- | ------------------------------------------------------------------ | | `country` | string | Yes | ISO 3166-1 alpha-2 country code (e.g. `US`, `JP`, `DE`) | | `isBusiness` | boolean | Yes | Whether this is a business purchase | | `postcode` | string | No | Postal or ZIP code | | `state` | string | Conditional | Required for `US` and `CA` (e.g. `CA`, `NY`, `ON`) | | `businessName` | string | Conditional | Required when `isBusiness` is `true` | | `taxId` | string | Conditional | Required for EU countries when `isBusiness` is `true` (VAT number) | ## Session Locks When a checkout session is created, the following values are locked and cannot change during the session lifetime: `productVersionId`, `productName`, `priceInfo`, `storeName`, `billingPeriod`, `withTrial`, `theme`, `buyerEmail`, `billingDetail` **Email normalization**: All email fields (`email`, `buyerEmail`, `contactEmail`) are normalized server-side via `trim().toLowerCase()` before storage, cache writes, and downstream calls. `Foo@Bar.COM` and `foo@bar.com` are treated as the same account. ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const session = await client.checkout.createSession({ productId: "PROD_7J3K5L8M2N4P6Q9R", currency: "USD", buyerEmail: "customer@example.com", successUrl: "https://example.com/thank-you", }); console.log(session.sessionId); // "cs_550e8400-e29b-41d4-a716-446655440000" console.log(session.checkoutUrl); // "https://checkout.waffo.ai/..." console.log(session.expiresAt); // "2026-01-22T10:30:00.000Z" ``` ```bash cURL (API Key) theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: MER_2D5F8G3H1K4M6N9P" \ -H "X-Timestamp: 1711800000" \ -H "X-Signature: BASE64_RSA_SIGNATURE" \ -d '{ "productId": "PROD_7J3K5L8M2N4P6Q9R", "currency": "USD", "buyerEmail": "customer@example.com", "successUrl": "https://example.com/thank-you" }' ``` ```bash cURL (Store Slug) theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "X-Store-Slug: my-store-abc123" \ -H "X-Environment: test" \ -d '{ "productId": "PROD_7J3K5L8M2N4P6Q9R", "currency": "USD" }' ``` ```python Python theme={"system"} import requests response = requests.post( "https://api.waffo.ai/v1/actions/checkout/create-session", headers={ "Content-Type": "application/json", "X-Merchant-Id": "MER_2D5F8G3H1K4M6N9P", "X-Timestamp": "1711800000", "X-Signature": "BASE64_RSA_SIGNATURE", }, json={ "productId": "PROD_7J3K5L8M2N4P6Q9R", "currency": "USD", "buyerEmail": "customer@example.com", }, ) data = response.json()["data"] print(data["checkoutUrl"]) ``` ## Success Response (200) ```json theme={"system"} { "data": { "sessionId": "cs_550e8400-e29b-41d4-a716-446655440000", "checkoutUrl": "https://checkout.waffo.ai/my-store-abc123/checkout/cs_550e8400-e29b-41d4-a716-446655440000", "expiresAt": "2026-01-22T10:30:00.000Z" } } ``` ## Response Fields | Field | Type | Description | | ------------- | ------ | ------------------------------------------------------------- | | `sessionId` | string | Checkout session ID (`cs_` + UUID format, not a Short ID) | | `checkoutUrl` | string | Full URL to redirect the consumer to the hosted checkout page | | `expiresAt` | string | ISO 8601 timestamp when the session expires | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing or invalid header: x-context-environment` | Environment header missing or not `test` / `prod` | Fix the header, resubmit | | 400 | `Invalid JSON body` | Request body is not valid JSON | Fix the body, resubmit | | 400 | `Missing required fields: productId, currency` | `productId` or `currency` is missing | Fix the body, resubmit | | 400 | `Invalid currency format: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY")` | Currency is not 3 uppercase letters (ISO 4217) | Use a valid ISO 4217 code, resubmit | | 400 | `Invalid billingDetail` | Billing detail validation failed (e.g. missing `state` for US) | Fix billing detail, resubmit | | 400 | `Expected format: PROD_xxx, got "..."` | `productId` Short ID could not be decoded | Fix `productId` format, resubmit | | 400 | `orderMerchantExternalId must not be empty` | Field was provided but is empty after trim | Omit the field or pass a non-empty value | | 400 | `orderMerchantExternalId must be at most 128 characters` | Value exceeds 128 chars | Shorten to 128 chars or less | | 400 | `Currency X is not supported for this product` | Product has no price snapshot for the requested currency | Use a currency supported by the product | | 401 | `Unauthorized` | Invalid API Key signature or Store Slug | Verify auth headers | | 403 | `Store is not active` | Store status is not `active` | Activate the store first | | 403 | `Store is not approved for production payments` | Store is not enabled for `prod` environment | Use `test` environment, or get the store approved | | 403 | `Product does not belong to this store` | Product is not owned by the visiting store | Verify the product/store pair | | 404 | `Store not found` | No store matches the given slug | Verify slug and environment | | 404 | `Product not found` | Product does not exist | Verify the `productId` | | 404 | `Product not found or not active for this environment` | Product has no active version for the given environment | Activate a product version for this environment | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | The default session TTL is 45 minutes (2700 seconds). When using API Key auth, you can customize this with `expiresInSeconds`. Sessions lock the product version and pricing at creation time, so price changes will not affect existing sessions. # Create One-Time Order Source: https://docs.waffo.ai/api-reference/endpoints/orders/create-onetime-order Create a one-time purchase order from a checkout session and obtain the PSP payment URL Create a one-time purchase order from an existing checkout session and return a hosted PSP payment URL. ``` POST /v1/actions/onetime-order/create-order ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer or shopper role) ## Checkout Flow 1. The merchant first calls [Create Checkout Session](/api-reference/endpoints/orders/create-checkout-session) to lock product version, pricing, and currency, and receives a `sessionId` 2. The buyer is sent to the hosted checkout page (or your own page if you handle the UI), where they confirm billing details 3. This endpoint is called to finalize the order — the server snapshots the price, calculates tax against `billingDetail`, writes the order, and creates the PSP checkout 4. The buyer is redirected to the returned `checkoutUrl` to complete payment * **Price snapshot**: the price is frozen at order creation time; later product price changes do not affect this order * **Tax calculation**: tax is computed from `billingDetail` (country, state for US/CA, business + VAT for EU) * **PSP fault tolerance**: if the PSP call fails, the order is rolled back so you can safely retry * **Email normalization**: `buyerEmail` is server-normalized with `trim().toLowerCase()` before tax calculation, order storage, session writeback, and PSP calls — `Foo@Bar.COM` and `foo@bar.com` are treated as the same buyer ## Request Body | Field | Type | Required | Description | | ------------------- | ------ | ----------- | ----------------------------------------------------------------------------------------------------------------------- | | `checkoutSessionId` | string | Yes | Checkout session ID returned by Create Checkout Session | | `billingDetail` | object | Conditional | Buyer billing detail (see below). Required unless already pre-filled on the session | | `buyerEmail` | string | Conditional | Buyer email. Required unless already pre-filled on the session; ignored when the session token carries a buyer identity | | `buyerIp` | string | No | Buyer IP address (used to refine tax calculation) | | `userTerminal` | string | No | Buyer terminal: `web` or `app`. Defaults to `web`. Forwarded to the payment provider | | `successUrl` | string | No | Override the success redirect URL configured on the session | `storeId`, `productId`, and `currency` are already locked on the checkout session — passing them in the body is ignored. ### Billing Detail Object | Field | Type | Required | Description | | -------------- | ------- | ----------- | ------------------------------------------------------------------ | | `country` | string | Yes | ISO 3166-1 alpha-2 country code | | `isBusiness` | boolean | Yes | Whether this is a business purchase | | `postcode` | string | No | Postal or ZIP code | | `state` | string | Conditional | Required for `US` and `CA` | | `businessName` | string | Conditional | Required when `isBusiness` is `true` | | `taxId` | string | Conditional | Required for EU countries when `isBusiness` is `true` (VAT number) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.createOnetime({ checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", buyerEmail: "customer@example.com", billingDetail: { country: "US", isBusiness: false, state: "CA", }, successUrl: "https://example.com/thank-you", }); console.log(result.checkoutUrl); // "https://checkout.stripe.com/c/pay/cs_xxx" ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/onetime-order/create-order", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", buyerEmail: "customer@example.com", billingDetail: { country: "US", isBusiness: false, state: "CA" }, successUrl: "https://example.com/thank-you", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/onetime-order/create-order" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerEmail":"customer@example.com","billingDetail":{"country":"US","isBusiness":false,"state":"CA"},"successUrl":"https://example.com/thank-you"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerEmail":"customer@example.com","billingDetail":{"country":"US","isBusiness":false,"state":"CA"},"successUrl":"https://example.com/thank-you"}' \ "https://api.waffo.ai/v1/actions/onetime-order/create-order" ``` ## Success Response (200) ```json theme={"system"} { "data": { "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_xxx" } } ``` ### Response Fields | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------- | | `checkoutUrl` | string | Hosted PSP payment URL — redirect the buyer here to complete payment | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------------------------------------------------- | ----------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Invalid JSON body` | Request body is not valid JSON | Fix the body, resubmit | | 400 | `Missing required field: checkoutSessionId` | `checkoutSessionId` was not provided | Fix the body, resubmit | | 400 | `Session product type mismatch: expected onetime` | The session was created for a subscription product | Create a new one-time session and retry | | 400 | `Environment mismatch between request and session` | `X-Environment` differs from the session's environment | Use the matching environment, resubmit | | 400 | `Missing billingDetail: provide in request body or pre-fill in checkout session` | Neither the request nor the session carries `billingDetail` | Provide `billingDetail`, resubmit | | 400 | `State is required for US/CA` | `billingDetail` validation failed | Fix `billingDetail`, resubmit | | 400 | `Missing buyerEmail: provide in request body or pre-fill in checkout session` | Neither the request nor the session carries a buyer email | Provide `buyerEmail`, resubmit | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Session does not belong to this store` | The session was created for a different store | Use a session belonging to the caller's store | | 409 | `Checkout session invalid, please re-enter checkout` | Session does not exist or has expired | Create a new checkout session and restart the flow | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Order Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/orders/overview Create checkout sessions and manage orders The order flow follows a session-based checkout pattern: 1. **Create a checkout session** -- locks the product version, pricing, and currency 2. **Preview tax** -- calculates tax based on the consumer's billing details 3. **Create an order** -- initiates payment and returns a PSP-hosted checkout URL 4. **Cancel an order** -- cancels a pending (unpaid) order before payment completes ## One-Time Order Status Values | Status | Description | | ----------- | ------------------------------------------------------------ | | `pending` | Order created, awaiting payment. Can be canceled. | | `completed` | Payment succeeded, order fulfilled. | | `canceled` | Order canceled before payment was completed. Terminal state. | ## Endpoints Lock product version and pricing for checkout Calculate tax before creating an order Create an order and get the payment URL Cancel a pending order before payment # Preview Trial Source: https://docs.waffo.ai/api-reference/endpoints/orders/preview-trial Preview the trial eligibility and trial days for a subscription checkout session Preview the trial period for a subscription checkout session — returns whether the buyer is eligible for a trial and the trial day count. ``` POST /v1/actions/checkout/preview-trial ``` **Authentication:** Store Slug (visitor) — also accepts a customer session token This endpoint also accepts a customer session token — see [Customer Session Endpoints](/api-reference/endpoints/auth/customer-endpoints). When called with a customer token, the buyer identity is taken from the token and overrides any `buyerIdentity` field in the request body. ## Eligibility Rule Eligibility is based on whether the buyer already has a trial record on this product (or any product in the same product group): * No prior trial record → `isEligible: true`, `trialDays` reflects the product's configured trial length * Trial record exists → `isEligible: false`, `trialDays` is `null` A visitor call with no `buyerIdentity` is treated as a brand-new buyer and always returns `isEligible: true`. ## Request Body | Field | Type | Required | Description | | ------------------- | ------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `checkoutSessionId` | string | Yes | Checkout session ID returned by Create Checkout Session (must be a subscription product) | | `buyerIdentity` | string | Conditional | Buyer email. Required in customer mode (taken from the token); optional in visitor mode — omitting it returns the full trial without record lookup | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const result = await client.checkout.previewTrial({ storeSlug: "my-store-abc123", checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", buyerIdentity: "buyer@example.com", }); console.log(result.isEligible); // true console.log(result.trialDays); // 14 ``` ```typescript TypeScript (Manual) theme={"system"} // Assumes callApi() is defined as shown in the Authentication guide const result = await callApi("POST", "/v1/actions/checkout/preview-trial", { checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", buyerIdentity: "buyer@example.com", }); ``` ```java Java theme={"system"} // Assumes callApi() is defined as shown in the Authentication guide String result = callApi("POST", "/v1/actions/checkout/preview-trial", "{\"checkoutSessionId\":\"cs_550e8400-e29b-41d4-a716-446655440000\"," + "\"buyerIdentity\":\"buyer@example.com\"}"); ``` ```python Python theme={"system"} # Assumes call_api() is defined as shown in the Authentication guide result = call_api("POST", "/v1/actions/checkout/preview-trial", { "checkoutSessionId": "cs_550e8400-e29b-41d4-a716-446655440000", "buyerIdentity": "buyer@example.com", }) ``` ```go Go theme={"system"} // Assumes callAPI() is defined as shown in the Authentication guide result, err := callAPI("POST", "/v1/actions/checkout/preview-trial", `{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerIdentity":"buyer@example.com"}`) ``` ```rust Rust theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide let result = call_api("POST", "/v1/actions/checkout/preview-trial", r#"{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerIdentity":"buyer@example.com"}"# ).await?; ``` ```c C theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide call_api("/v1/actions/checkout/preview-trial", "{\"checkoutSessionId\":\"cs_550e8400-e29b-41d4-a716-446655440000\"," "\"buyerIdentity\":\"buyer@example.com\"}"); ``` ```cpp C++ theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide auto result = call_api("/v1/actions/checkout/preview-trial", R"({"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerIdentity":"buyer@example.com"})"); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/checkout/preview-trial" \ -H "Content-Type: application/json" \ -H "X-Store-Slug: my-store-abc123" \ -H "X-Environment: test" \ -d '{ "checkoutSessionId": "cs_550e8400-e29b-41d4-a716-446655440000", "buyerIdentity": "buyer@example.com" }' ``` ```bash wget theme={"system"} wget -qO- \ --header="Content-Type: application/json" \ --header="X-Store-Slug: my-store-abc123" \ --header="X-Environment: test" \ --post-data='{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","buyerIdentity":"buyer@example.com"}' \ "https://api.waffo.ai/v1/actions/checkout/preview-trial" ``` ## Success Response (200) -- Eligible ```json theme={"system"} { "data": { "isEligible": true, "trialDays": 14 } } ``` ## Success Response (200) -- Not Eligible ```json theme={"system"} { "data": { "isEligible": false, "trialDays": null } } ``` ### Response Fields | Field | Type | Description | | ------------ | -------------- | -------------------------------------------------------------------------- | | `isEligible` | boolean | Whether the buyer is eligible for a trial (no prior trial record = `true`) | | `trialDays` | number \| null | Trial day count when eligible; `null` when not eligible | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------ | ---------------------------------------------- | --------------------------------------------------------- | | 400 | `Invalid JSON body` | Request body is not valid JSON | Fix the body, resubmit | | 400 | `Missing required field: checkoutSessionId` | `checkoutSessionId` was not provided | Fix the body, resubmit | | 400 | `Session product type mismatch: expected subscription` | The session was created for a one-time product | Create a subscription checkout session and retry | | 401 | `Unauthorized` | Invalid Store Slug or session token | Verify auth headers | | 404 | `Session not found` | Checkout session does not exist or has expired | Create a new checkout session and retry | | 404 | `Session does not belong to this store` | The session was created for a different store | Use a session belonging to the calling store | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Create Refund Ticket Source: https://docs.waffo.ai/api-reference/endpoints/refunds/create-refund-ticket Request a refund for a previously succeeded payment `POST /api/actions/refund-ticket/create-ticket` **Authentication:** Merchant API Key (server-to-server). ## Request body | Field | Type | Required | Description | | -------------------------------- | ------ | -------- | -------------------------------------------------------------------------------------- | | `paymentId` | string | Yes | Short ID of the payment to refund, e.g. `PAY_6eYCunG3IMmIgcQOnaXdoA` | | `reason` | string | Yes | Free-text reason shown to the consumer / dashboard | | `requestedAmount.amount` | string | Yes | Refund amount in display format (e.g. `"10.50"`), must be `> 0` and `≤ payment.amount` | | `requestedAmount.currency` | string | Yes | ISO 4217 code, must match the payment currency | | `refundTicketMerchantExternalId` | string | No | Your business-side refund-ticket reference (max 128 chars). | ## Success response (200) ```json theme={"system"} { "data": { "ticket": { "id": "TKT_3bVzrkD0FJjFdZNLk8Ualx", "status": "processing", "subjectId": "PAY_6eYCunG3IMmIgcQOnaXdoA", "refundTicketMerchantExternalId": "REF-2026-00891" } } } ``` When the merchant submits via API Key, the ticket is auto-approved and goes straight to `processing`. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | 400 | `Missing required field: ` | Required field missing (`paymentId`, `reason`, `requestedAmount.amount`, `requestedAmount.currency`) | Fix the body, resubmit | | 400 | `Expected format: PAY_xxx, got "..."` | `paymentId` Short ID could not be decoded | Fix `paymentId` format, resubmit | | 400 | `Invalid requestedAmount.amount format` | Amount string could not be parsed | Use a display string like `"10.50"` (not minor units) | | 400 | `refundTicketMerchantExternalId must be at most 128 characters` | Value exceeds 128 chars | Shorten to 128 chars or less | | 401 | `Unauthorized` | Invalid API Key signature | Verify auth headers | | 404 | `Payment not found` | Payment doesn't exist or doesn't belong to your store | Verify `paymentId` | | 409 | `Payment status is X, must be succeeded` | Original payment is not in `succeeded` state | Do not retry — the payment never succeeded | | 409 | `Payment is not yet fully processed by PSP, please retry later` | PSP confirmation hasn't been written back yet | Retry after 5–30 seconds (transient) | | 409 | `Refund period expired (X days, max 14)` | Past the 14-day window | Do not retry — refunds are no longer allowed | | 409 | `Requested amount must be greater than 0` | Amount is ≤ 0 | Fix the amount, resubmit | | 409 | `Requested amount exceeds payment amount` | Amount > `payment.amount` | Fix the amount, resubmit | | 409 | `Currency mismatch: requested X, payment Y` | Currency must match the original payment | Use the payment's currency | | 409 | `A refund record already exists for this payment` | A previous refund already succeeded against this payment | Do not retry — query existing refunds via GraphQL | | 409 | `A refund ticket already exists for this payment` | Another in-flight ticket is blocking | Wait for the existing ticket to terminate, then resubmit if still needed | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | Use [GraphQL](/api-reference/endpoints/graphql/orders-and-payments) to query the created ticket and the executed refund record. Filter executed refunds by the ticket reference using `refunds(filter: { refundTicketMerchantExternalId: ... })`. # Refund Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/refunds/overview Request refunds and query refund records via REST + GraphQL Refund endpoints allow merchants to issue refunds for previously succeeded payments. Refunds are modelled as **refund tickets** (the request) and **refund records** (the executed result, written after PSP confirms). *** ## Business Rules * Refunds must be requested within **14 days** of the original payment success * Refund amount can be **partial** (any value `> 0` and `≤ payment.amount`), but must use the **same currency** as the payment * The original payment must already be **fully confirmed by the PSP** — calling refund immediately after a payment webhook arrives may transiently fail; retry after a few seconds * A payment that already has a pending or succeeded refund **cannot have another refund ticket created** at the same time * When the merchant submits the refund (server-to-server via API Key), the ticket is **auto-approved** and goes straight to processing — no manual review step *** ## Refund Ticket Status Values | Status | Meaning for merchant | | ------------ | ----------------------------------------------------------- | | `pending` | Awaiting Waffo review (only seen when buyer self-initiated) | | `approved` | Approved, will be dispatched to PSP shortly | | `rejected` | Rejected — see `rejectReason` in GraphQL | | `processing` | Dispatched to PSP, awaiting upstream completion | | `succeeded` | Funds returned to the consumer | | `failed` | PSP returned failure — can be reviewed and resubmitted | Merchant-initiated refunds skip `pending` and `approved` entirely — they appear as `processing` immediately after creation. *** ## Business identifiers — link your records to Waffo You can attach an optional business-side identifier (max **128 characters**) at two creation points; on the wire each one carries its own flat key so a single payload can hold both without ambiguity: | Created at | Request body field | Stored as | | ----------------------------- | -------------------------------- | ------------------------------------------------------------------------------- | | `checkout/create-session` | `orderMerchantExternalId` | `orders.merchant_external_id` (inherited by first and renewal payments) | | `refund-ticket/create-ticket` | `refundTicketMerchantExternalId` | `refund_tickets.merchant_external_id` (inherited by the executed refund record) | End-to-end propagation (the same flat dual-key names are used everywhere — request bodies, webhook payloads, and GraphQL types): ``` checkout/create-session (request: orderMerchantExternalId) │ ├──► OnetimeOrder / SubscriptionOrder . orderMerchantExternalId ├──► Payment . orderMerchantExternalId (first + subscription renewals) │ refund-ticket/create-ticket (request: refundTicketMerchantExternalId) │ ├──► RefundTicket . refundTicketMerchantExternalId │ └──► Refund carries BOTH: • orderMerchantExternalId (from the originating order) • refundTicketMerchantExternalId (from the originating refund ticket) ``` These identifiers are **not** idempotency keys — Waffo does not enforce uniqueness; the same reference can be attached to multiple records and you are responsible for any uniqueness guarantees on your side. The same field names appear in three places — request body, webhook payload, GraphQL — so the value you write at checkout/refund-ticket creation can be read back without renaming. Use these identifiers to **query** Payments, Refunds, and Refund Tickets via GraphQL — see the [GraphQL orders & payments](/api-reference/endpoints/graphql/orders-and-payments) guide. *** ## Endpoints Request a refund for a succeeded payment. Supports full and partial refunds. Revise and resubmit a rejected or failed refund ticket. Use [GraphQL](/api-reference/endpoints/graphql/orders-and-payments) to query refund tickets and the executed refund records. On `Refund`, the two business numbers are exposed as flat fields `refund.orderMerchantExternalId` and `refund.refundTicketMerchantExternalId` — same names as the webhook payload. Filter executed refunds by the refund ticket reference using `refunds(filter: { refundTicketMerchantExternalId: ... })`. # Resubmit Refund Ticket Source: https://docs.waffo.ai/api-reference/endpoints/refunds/resubmit-refund-ticket Revise and resubmit a rejected or failed refund ticket `POST /api/actions/refund-ticket/resubmit-ticket` **Authentication:** Merchant API Key (server-to-server). Resubmit reuses the request shape of [Create Refund Ticket](/api-reference/endpoints/refunds/create-refund-ticket), plus a `ticketId` to identify the ticket being revised. The originating `paymentId` is bound to the existing ticket and cannot be changed. ## Request body | Field | Type | Required | Description | | -------------------------- | ------ | -------- | ------------------------------------------------------------------------------ | | `ticketId` | string | Yes | Short ID of the refund ticket being revised, e.g. `TKT_3bVzrkD0FJjFdZNLk8Ualx` | | `reason` | string | Yes | Free-text reason for the revised request | | `requestedAmount.amount` | string | Yes | Revised refund amount (display format, must be `> 0` and `≤ payment.amount`) | | `requestedAmount.currency` | string | Yes | ISO 4217 code, must match the payment currency | The `refundTicketMerchantExternalId` written at create time is **immutable** across resubmits — it cannot be changed once the ticket exists. ## Success response (200) ```json theme={"system"} { "data": { "ticket": { "id": "TKT_3bVzrkD0FJjFdZNLk8Ualx", "status": "pending", "subjectId": "PAY_6eYCunG3IMmIgcQOnaXdoA", "versionNumber": 2, "refundTicketMerchantExternalId": "REF-2026-00891" } } } ``` A new ticket version is appended; `versionNumber` increments. The ticket re-enters the review queue (`pending` for buyer-initiated, `processing` for merchant-initiated). ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: ticketId` | `ticketId` not provided | Add `ticketId`, resubmit | | 400 | `Expected format: TKT_xxx, got "..."` | `ticketId` Short ID could not be decoded | Fix `ticketId` format, resubmit | | 400 | `Invalid requestedAmount.amount format` | Amount string could not be parsed | Use a display string like `"10.50"` | | 401 | `Unauthorized` | Invalid API Key signature | Verify auth headers | | 403 | `Submitter is not the original ticket owner` | The caller didn't create this ticket | Only the original submitter can resubmit | | 404 | `Ticket not found` | Ticket doesn't exist or doesn't belong to the caller | Verify `ticketId` | | 409 | `Ticket status is X, only rejected/failed can be resubmitted` | Ticket is not in a resubmittable state | Wait for `rejected` / `failed`, or open a new ticket | | 409 | `Requested amount exceeds payment amount` | Revised amount > `payment.amount` | Fix the amount | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Create Store Source: https://docs.waffo.ai/api-reference/endpoints/stores/create-store Create a new store for the authenticated merchant Create a new store for the authenticated merchant. The store slug is auto-generated from the name, and JSONB configuration fields (`notificationSettings`, `checkoutSettings`) are initialized with defaults. Webhooks are managed separately via [`add-webhook`](/api-reference/endpoints/webhooks/add-webhook) — no rows exist on a freshly created store. ``` POST /v1/actions/store/create-store ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | ------ | ------ | -------- | ------------------------------------------ | | `name` | string | Yes | Store name (1-48 characters, auto-trimmed) | ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { store } = await client.stores.create({ name: "My Digital Store" }); console.log(store.id); // => "STO_2aUyqjCzEIiEcYMKj7TZtw" console.log(store.slug); // => "my-digital-store-a1b2c3" ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/store/create-store", { name: "My Digital Store", }); console.log(result.data.store.id); // => "STO_2aUyqjCzEIiEcYMKj7TZtw" console.log(result.data.store.slug); // => "my-digital-store-a1b2c3" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"name":"My Digital Store"}"""; String response = callApi("POST", "/v1/actions/store/create-store", body); System.out.println(response); // => {"data":{"store":{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","slug":"my-digital-store-a1b2c3",...}}} ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/store/create-store", { "name": "My Digital Store", }) store = result["data"]["store"] print(store["id"]) # => "STO_2aUyqjCzEIiEcYMKj7TZtw" print(store["slug"]) # => "my-digital-store-a1b2c3" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "name": "My Digital Store", } result, err := callAPI("POST", "/v1/actions/store/create-store", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) // => {"data":{"store":{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","slug":"my-digital-store-a1b2c3",...}}} ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "name": "My Digital Store" }); let result = call_api("POST", "/v1/actions/store/create-store", &body).await?; println!("{}", result); // => {"data":{"store":{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","slug":"my-digital-store-a1b2c3",...}}} ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"name\":\"My Digital Store\"}"; char *response = call_api("POST", "/v1/actions/store/create-store", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"name":"My Digital Store"})"; std::string response = callApi("POST", "/v1/actions/store/create-store", body); std::cout << response << std::endl; // => {"data":{"store":{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","slug":"my-digital-store-a1b2c3",...}}} ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"name":"My Digital Store"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/create-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/store/create-store" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"name":"My Digital Store"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/create-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/store/create-store" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "store": { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "My Digital Store", "status": "active", "logo": null, "supportEmail": null, "website": null, "slug": "my-digital-store-a1b2c3", "prodEnabled": false, "notificationSettings": { "emailOrderConfirmation": true, "emailSubscriptionConfirmation": true, "emailSubscriptionCycled": true, "emailSubscriptionCanceled": true, "emailSubscriptionRevoked": true, "emailSubscriptionPastDue": true, "notifyNewOrders": true, "notifyNewSubscriptions": true }, "checkoutSettings": { "defaultDarkMode": false, "light": { "checkoutLogo": null, "checkoutColorPrimary": "#000000", "checkoutColorBackground": "#FFFFFF", "checkoutColorCard": "#F5F5F5", "checkoutColorText": "#1A1A1A", "checkoutBorderRadius": "8px" }, "dark": { "checkoutLogo": null, "checkoutColorPrimary": "#FFFFFF", "checkoutColorBackground": "#1A1A1A", "checkoutColorCard": "#2A2A2A", "checkoutColorText": "#F5F5F5", "checkoutBorderRadius": "8px" } }, "deletedAt": null, "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T10:30:00.000Z" } } } ``` ## Response Fields | Field | Type | Description | | ---------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Store ID (Short ID format `STO_xxx`) | | `name` | string | Store display name | | `status` | string | Store status (`active`, `inactive`, or `suspended`) | | `logo` | string \| null | Store logo URL | | `supportEmail` | string \| null | Support email address | | `website` | string \| null | Store website URL | | `slug` | string \| null | Auto-generated URL slug | | `prodEnabled` | boolean | Whether production mode is enabled | | `notificationSettings` | object \| null | Notification preferences (see [Notification Settings](/api-reference/endpoints/stores/update-store#notification-settings)) | | `checkoutSettings` | object \| null | Checkout page theme (see [Checkout Settings](/api-reference/endpoints/stores/update-store#checkout-settings)) | | `deletedAt` | string \| null | Soft-delete timestamp (ISO 8601), `null` if active | | `createdAt` | string | Creation timestamp (ISO 8601) | | `updatedAt` | string | Last update timestamp (ISO 8601) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------- | | 400 | `Missing merchantId in request context` | API Key did not resolve to a merchant context | **Do not retry.** Check your API Key configuration. | | 400 | `Missing required field: name` | `name` is absent from the body | Fix the input and resubmit. | | 400 | `Store name cannot be empty or contain only whitespace` | `name` is empty after `trim()` | Fix the input and resubmit. | | 400 | `Store name cannot exceed 48 characters` | `name` is longer than 48 characters | Shorten the name and resubmit. | | 400 | `Cannot create more stores. Maximum limit of 20 stores per merchant has been reached.` | The merchant already owns 20 stores | **Do not retry.** Delete an existing store first, or contact support to raise the limit. | Each merchant can create up to **20 stores**. The store creator is automatically assigned the `owner` role. # Delete Store Source: https://docs.waffo.ai/api-reference/endpoints/stores/delete-store Soft-delete a store (owner only) Soft-delete a store. The store data is retained but becomes inaccessible. Only the store **owner** can perform this action. ``` POST /v1/actions/store/delete-store ``` **Authentication:** API Key (owner role required) ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | ---------------------------------------------- | | `id` | string | Yes | Store ID to delete (Short ID format `STO_xxx`) | ## Example Request ```typescript SDK theme={"system"} const { store } = await client.stores.delete({ id: "STO_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(store.deletedAt); // => "2026-01-15T12:00:00.000Z" ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/store/delete-store", { id: "STO_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(result.data.store.deletedAt); // => "2026-01-15T12:00:00.000Z" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"STO_2aUyqjCzEIiEcYMKj7TZtw"}"""; String response = callApi("POST", "/v1/actions/store/delete-store", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/store/delete-store", { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", }) store = result["data"]["store"] print(store["deletedAt"]) # => "2026-01-15T12:00:00.000Z" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", } result, err := callAPI("POST", "/v1/actions/store/delete-store", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "STO_2aUyqjCzEIiEcYMKj7TZtw" }); let result = call_api("POST", "/v1/actions/store/delete-store", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"STO_2aUyqjCzEIiEcYMKj7TZtw\"}"; char *response = call_api("POST", "/v1/actions/store/delete-store", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"STO_2aUyqjCzEIiEcYMKj7TZtw"})"; std::string response = callApi("POST", "/v1/actions/store/delete-store", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/delete-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/store/delete-store" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/delete-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/store/delete-store" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "store": { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "My Digital Store", "status": "active", "logo": null, "supportEmail": null, "website": null, "slug": "my-digital-store-a1b2c3", "prodEnabled": false, "notificationSettings": null, "checkoutSettings": null, "deletedAt": "2026-01-15T12:00:00.000Z", "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T12:00:00.000Z" } } } ``` ## Response Fields Same as [Create Store response fields](/api-reference/endpoints/stores/create-store#response-fields), with `deletedAt` populated. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). On 409, clean up blocking resources first (AI callers should escalate via `aiHint`). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------- | | 400 | `Missing merchantId in request context` | API Key did not resolve to a merchant context | **Do not retry.** Check your API Key configuration. | | 400 | `Missing required field: id` | `id` is absent from the body | Fix the input and resubmit. | | 400 | `Expected format: STO_xxx, got ""` | `id` is not a valid Store Short ID | Fix the `id` value and resubmit. | | 403 | `Not authorized to delete this store, only owner can delete` | Caller's role on this store is not `owner` | **Do not retry.** Use an `owner` API Key. | | 404 | `Store not found` | Store ID does not exist for this merchant | **Do not retry.** Verify the `id`. | | 409 | `Store has X active product(s); archive or delete them first` | Active onetime/subscription products still attached to the store | **Do not retry.** Archive or delete those products, then resubmit. | | 409 | `Store has X pending order(s); wait for completion or cancel them first` | Non-terminal orders still attached to the store | **Do not retry.** Wait for completion or cancel them, then resubmit. | | 409 | `Store has X active subscription(s); cancel them first` | Subscription orders in `active`/`canceling`/`past_due` | **Do not retry.** Cancel the subscriptions, then resubmit. | | 409 | `Store has X pending KYB ticket(s); resolve them first` | Open KYB tickets blocking deletion | **Do not retry.** Resolve the tickets, then resubmit. | | 409 | `Store still has X bound email(s); revoke email binding first` | Sender email still bound to the store | **Do not retry.** Revoke the email binding, then resubmit. | | 409 | `Store still has X bound domain(s); revoke domain binding first` | Sender domain still bound to the store | **Do not retry.** Revoke the domain binding, then resubmit. | ### 409 Response Example When a store has blocking resources or bindings, the response returns one entry per category. Each entry carries `reason` (machine-readable), `count`, a human-readable `message`, and an `aiHint` instructing AI-driven callers to stop and escalate to a human operator instead of retrying. ```json theme={"system"} { "data": null, "errors": [ { "message": "Store has 3 active product(s); archive or delete them first", "layer": "store", "reason": "active_products", "count": 3, "aiHint": "AI assistant: stop this action and escalate to a human operator. Do not retry, mutate inputs, or attempt workarounds." }, { "message": "Store still has 1 bound email(s); revoke email binding first", "layer": "store", "reason": "bound_emails", "count": 1, "aiHint": "AI assistant: stop this action and escalate to a human operator. Do not retry, mutate inputs, or attempt workarounds." } ] } ``` | `reason` | Meaning | | ---------------------- | ----------------------------------------------------------------------------------------- | | `active_products` | Onetime / subscription products with `prod_status` or `test_status` = `active` | | `pending_orders` | Orders not in a terminal state (excludes `completed` / `canceled` / `closed` / `expired`) | | `active_subscriptions` | Subscription orders in `active` / `canceling` / `past_due` | | `pending_tickets` | Open KYB tickets (excludes `succeeded` / `rejected`) | | `bound_emails` | Sender email still bound — revoke the binding first | | `bound_domains` | Sender domain still bound — revoke the binding first | `aiHint` appears only on 409 responses; it is omitted on 400 / 403 / 404 / 500. Deleting a store is a **soft delete**. The store data is retained but the store becomes inaccessible. This action cannot be undone through the API. Before deleting, you must deactivate all products, resolve pending orders, cancel active subscriptions, close any open KYB tickets, and revoke any bound sender email or domain. # Store Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/stores/overview Create, update, and manage stores Stores are the top-level entity for organizing products, orders, and checkout experiences. Each merchant can own up to **20 stores**, and each store has its own webhook, notification, and checkout theme settings. ## Store Status Values | Status | Description | | ----------- | -------------------------------------------------------------------------------- | | `active` | Store is live and operational | | `inactive` | Store is disabled by the merchant; data is preserved but checkout is unavailable | | `suspended` | Store is suspended by the platform; merchant cannot reactivate | ## Role-Based Access Control Each merchant in a store is assigned one of three roles. The store creator is automatically assigned `owner`. | Permission | `owner` | `admin` | `member` | | --------------------- | ------- | ------- | -------- | | Read store details | Yes | Yes | Yes | | Update store settings | Yes | Yes | No | | Delete store | Yes | No | No | | Create store | -- | -- | -- | Store creation is not role-gated -- any authenticated merchant can create a new store (up to the 20-store limit). Roles only apply to operations on existing stores. ## Endpoints Create a new store for the authenticated merchant. Update store name, status, or configuration settings. Soft-delete a store (owner only). # Update Store Source: https://docs.waffo.ai/api-reference/endpoints/stores/update-store Update an existing store's name, status, or configuration settings Update an existing store's name, status, or configuration settings. Only fields included in the request body are updated; omitted fields remain unchanged. ``` POST /v1/actions/store/update-store ``` **Authentication:** API Key (owner or admin role required) This endpoint does not manage webhook configuration. If a `webhookSettings` field is sent in the request body it is silently ignored and the response includes a top-level `warnings` array; other fields update normally and the call returns 200. Use [`add-webhook`](/api-reference/endpoints/webhooks/add-webhook), [`update-webhook`](/api-reference/endpoints/webhooks/update-webhook), and [`remove-webhook`](/api-reference/endpoints/webhooks/remove-webhook) to configure webhooks, and GraphQL `Store.storeWebhooks` to list them. ## Request Body | Field | Type | Required | Description | | ---------------------- | ---------------- | -------- | ------------------------------------------------------ | | `id` | string | Yes | Store ID (Short ID format `STO_xxx`) | | `name` | string | No | Updated store name (1-48 characters) | | `status` | string | No | `active`, `inactive`, or `suspended` | | `logo` | string \| null | No | Store logo URL (set to `null` to remove) | | `supportEmail` | `string \| null` | No | Store support email address (set to `null` to remove) | | `website` | `string \| null` | No | Store website URL (set to `null` to remove) | | `notificationSettings` | object \| null | No | Notification preferences (set to `null` to remove) | | `checkoutSettings` | object \| null | No | Checkout theme configuration (set to `null` to remove) | ## Notification Settings Two categories with different write permissions: **Merchant-writable** (✅ accepted via this endpoint): | Field | Type | Default | Description | | ------------------------------ | ------- | ------- | ---------------------------------------------------------------------- | | `notifyNewOrders` | boolean | `true` | Notify merchant of new orders | | `notifyNewSubscriptions` | boolean | `true` | Notify merchant of new subscriptions | | `notifySubscriptionCanceled` | boolean | `true` | Notify merchant when a subscriber cancels (still within access period) | | `notifySubscriptionEnded` | boolean | `true` | Notify merchant when a subscription ends | | `notifySubscriptionPastDue` | boolean | `true` | Notify merchant when a subscription enters past-due (payment failure) | | `notifySubscriptionRenewed` | boolean | `true` | Notify merchant when a subscription successfully renews | | `notifySubscriptionUncanceled` | boolean | `true` | Notify merchant when a previously canceled subscription is reactivated | | `notifySubscriptionUpdated` | boolean | `true` | Notify merchant when a subscription plan changes (forward-compat) | | `notifyChargeback` | boolean | `true` | Notify merchant when a chargeback is filed (forward-compat) | | `notifyPayoutCompleted` | boolean | `true` | Notify merchant when a payout completes (forward-compat) | | `notifyPayoutFailed` | boolean | `true` | Notify merchant when a payout fails (forward-compat) | **Platform-managed** (🔒 read-only via merchant API; managed by PANCAKE platform): | Field | Type | Default | Description | | ------------------------------- | ------- | ------- | --------------------------------------- | | `emailOrderConfirmation` | boolean | `true` | Send email on order confirmation | | `emailSubscriptionConfirmation` | boolean | `true` | Send email on subscription creation | | `emailSubscriptionCycled` | boolean | `true` | Send email on subscription renewal | | `emailSubscriptionCanceled` | boolean | `true` | Send email on subscription cancellation | | `emailSubscriptionRevoked` | boolean | `true` | Send email on subscription revocation | | `emailSubscriptionPastDue` | boolean | `true` | Send email on subscription past due | | `emailTrialStarted` | boolean | `true` | Send email when a free trial is started | | `emailTrialEnding` | boolean | `true` | Send email reminder before a trial ends | If your `notificationSettings` payload includes any platform-managed `email*` field, the server silently drops it and returns a `200` response with a `warnings[]` entry listing the dropped keys. To toggle a consumer email, contact PANCAKE platform support. ## Checkout Settings | Field | Type | Description | | ----------------- | ------- | -------------------------------- | | `defaultDarkMode` | boolean | Whether to default to dark mode | | `light` | object | Light theme settings (see below) | | `dark` | object | Dark theme settings (see below) | **Checkout Theme Settings** (applies to both `light` and `dark`): | Field | Type | Description | | ------------------------- | -------------- | -------------------------- | | `checkoutLogo` | string \| null | Logo URL for checkout page | | `checkoutColorPrimary` | string | Primary color (hex) | | `checkoutColorBackground` | string | Background color (hex) | | `checkoutColorCard` | string | Card/panel color (hex) | | `checkoutColorText` | string | Text color (hex) | | `checkoutBorderRadius` | string | Border radius (CSS value) | ## Partial Update Semantics Both settings objects support partial updates. Each sub-field follows these semantics: | Value | Behavior | Example | | --------------------- | ------------------- | -------------------------------------------------------------------- | | Omitted (not in JSON) | Keep existing value | Only pass `notifyNewOrders`; other notification flags stay unchanged | | `null` | Clear the field | `"checkoutLogo": null` removes the checkout logo | | Actual value | Create or update | `"checkoutColorPrimary": "#FF6600"` sets a new primary color | Setting the entire object to `null` (e.g., `"notificationSettings": null`) clears all fields in that settings group. ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake, EntityStatus } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { store } = await client.stores.update({ id: "STO_2aUyqjCzEIiEcYMKj7TZtw", name: "Updated Store Name", notificationSettings: { notifyNewOrders: true, notifyNewSubscriptions: false, }, }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/store/update-store", { id: "STO_2aUyqjCzEIiEcYMKj7TZtw", name: "Updated Store Name", notificationSettings: { notifyNewSubscriptions: false, }, }); console.log(result.data.store.name); // => "Updated Store Name" ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "notificationSettings": { "notifyNewSubscriptions": false } }"""; String response = callApi("POST", "/v1/actions/store/update-store", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/store/update-store", { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "notificationSettings": { "notifyNewSubscriptions": False, }, }) store = result["data"]["store"] print(store["name"]) # => "Updated Store Name" ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "notificationSettings": map[string]interface{}{ "notifyNewSubscriptions": false, }, } result, err := callAPI("POST", "/v1/actions/store/update-store", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "notificationSettings": { "notifyNewSubscriptions": false } }); let result = call_api("POST", "/v1/actions/store/update-store", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"STO_2aUyqjCzEIiEcYMKj7TZtw\"," "\"name\":\"Updated Store Name\"," "\"notificationSettings\":{\"notifyNewSubscriptions\":false}}"; char *response = call_api("POST", "/v1/actions/store/update-store", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({ "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "notificationSettings": { "notifyNewSubscriptions": false } })"; std::string response = callApi("POST", "/v1/actions/store/update-store", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"Updated Store Name","notificationSettings":{"notifyNewSubscriptions":false}}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/update-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/store/update-store" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"STO_2aUyqjCzEIiEcYMKj7TZtw","name":"Updated Store Name","notificationSettings":{"notifyNewSubscriptions":false}}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/store/update-store $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/store/update-store" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "store": { "id": "STO_2aUyqjCzEIiEcYMKj7TZtw", "name": "Updated Store Name", "status": "active", "logo": null, "slug": "my-digital-store-a1b2c3", "prodEnabled": false, "notificationSettings": { "emailOrderConfirmation": true, "emailSubscriptionConfirmation": true, "emailSubscriptionCycled": true, "emailSubscriptionCanceled": true, "emailSubscriptionRevoked": true, "emailSubscriptionPastDue": true, "emailTrialStarted": true, "emailTrialEnding": true, "notifyNewOrders": true, "notifyNewSubscriptions": false, "notifySubscriptionCanceled": true, "notifySubscriptionEnded": true, "notifySubscriptionPastDue": true, "notifySubscriptionRenewed": true, "notifySubscriptionUncanceled": true, "notifySubscriptionUpdated": true, "notifyChargeback": true, "notifyPayoutCompleted": true, "notifyPayoutFailed": true }, "checkoutSettings": { "defaultDarkMode": false, "light": { "checkoutLogo": null, "checkoutColorPrimary": "#000000", "checkoutColorBackground": "#FFFFFF", "checkoutColorCard": "#F5F5F5", "checkoutColorText": "#1A1A1A", "checkoutBorderRadius": "8px" }, "dark": { "checkoutLogo": null, "checkoutColorPrimary": "#FFFFFF", "checkoutColorBackground": "#1A1A1A", "checkoutColorCard": "#2A2A2A", "checkoutColorText": "#F5F5F5", "checkoutBorderRadius": "8px" } }, "deletedAt": null, "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-01-15T11:00:00.000Z" } } } ``` ## Response Fields Same as [Create Store response fields](/api-reference/endpoints/stores/create-store#response-fields). ## webhookSettings Compatibility Warning If a `webhookSettings` field is sent in the request body, it is silently ignored and the response includes a top-level `warnings` array. The `data.store` object is unaffected. ```json theme={"system"} { "data": { "store": { "...": "..." } }, "warnings": [ { "message": "webhookSettings is no longer accepted on update-store; the field was ignored.", "layer": "store", "aiHint": "AI assistant: 'webhookSettings' is permanently removed (BREAKING 2026-05). Do not retry with this field. SDK users: upgrade to @waffo/pancake-ts >= 0.6.0 and call client.webhooks.add / update / remove instead of client.stores.update({ webhookSettings }). Direct API users: POST /api/actions/store/add-webhook to create a webhook, /update-webhook to modify, /remove-webhook to delete. Query the webhook list via GraphQL Store.storeWebhooks field, not via this endpoint." } ] } ``` ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------- | -------------------------------------------------- | --------------------------------------------------- | | 400 | `Missing merchantId in request context` | API Key did not resolve to a merchant context | **Do not retry.** Check your API Key configuration. | | 400 | `Missing required field: id` | `id` is absent from the body | Fix the input and resubmit. | | 400 | `Expected format: STO_xxx, got ""` | `id` is not a valid Store Short ID | Fix the `id` value and resubmit. | | 400 | `Store name cannot be empty or contain only whitespace` | `name` is empty after `trim()` | Fix the `name` and resubmit. | | 400 | `Store name cannot exceed 48 characters` | `name` is longer than 48 characters | Shorten the `name` and resubmit. | | 400 | `Invalid status, must be active, inactive or suspended` | `status` value is not one of the allowed enums | Use `active`, `inactive`, or `suspended`. | | 400 | `Invalid logo: must be a string or null` | `logo` is not a string and not `null` | Pass a string URL or `null` to clear. | | 403 | `Not authorized to update this store` | Caller's role on this store is not `owner`/`admin` | **Do not retry.** Use an authorized merchant. | | 404 | `Store not found` | Store ID does not exist for this merchant | **Do not retry.** Verify the `id`. | # Create Group Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/create-group Create a product group to organize related subscription products Create a product group to organize related subscription products. ``` POST /v1/actions/subscription-product-group/create-group ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | ------------- | --------- | -------- | ------------------------------------------------------- | | `storeId` | string | Yes | Store ID (`STO_xxx` format) | | `name` | string | Yes | Group name (unique per store + environment) | | `description` | string | No | Group description | | `rules` | object | No | Group rules (`{ sharedTrial: boolean }`) | | `productIds` | string\[] | No | Subscription product IDs (`PROD_xxx` format) to include | ## Example Request ```typescript SDK theme={"system"} const { group } = await client.subscriptionProductGroups.create({ storeId: "STO_2D5F8G3H1K4M6N9P", name: "Pricing Plans", description: "Free, Pro, and Enterprise tiers", rules: { sharedTrial: true }, productIds: [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", "PROD_1C3E5G7J0L2N4Q6S", ], }); ``` ```bash cURL theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/subscription-product-group/create-group \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: MER_7K3M9P2R5T8W1Y4Z" \ -H "X-Timestamp: 2026-03-30T10:00:00Z" \ -H "X-Signature: BASE64_RSA_SIGNATURE" \ -d '{ "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pricing Plans", "description": "Free, Pro, and Enterprise tiers", "rules": { "sharedTrial": true }, "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", "PROD_1C3E5G7J0L2N4Q6S" ] }' ``` ```python Python theme={"system"} body = { "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pricing Plans", "description": "Free, Pro, and Enterprise tiers", "rules": {"sharedTrial": True}, "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", "PROD_1C3E5G7J0L2N4Q6S", ], } response = requests.post( "https://api.waffo.ai/v1/actions/subscription-product-group/create-group", json=body, headers=sign_request("POST", "/v1/actions/subscription-product-group/create-group", body), ) group = response.json()["data"]["group"] ``` ## Success Response (200) ```json theme={"system"} { "data": { "group": { "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pricing Plans", "description": "Free, Pro, and Enterprise tiers", "rules": { "sharedTrial": true }, "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", "PROD_1C3E5G7J0L2N4Q6S" ], "environment": "test", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T10:30:00.000Z" } } } ``` ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing or invalid header: x-context-environment` | Environment header missing or not `test` / `prod` | Fix the header, resubmit | | 400 | `Missing required fields: storeId, name` | A required body field was omitted | Add the missing field, resubmit | | 400 | `Expected format: STO_xxx, got "..."` / `Expected format: PROD_xxx, got "..."` | `storeId` or a `productIds` entry could not be decoded as a Short ID | Fix the ID, resubmit | | 500 | `Internal server error` | Internal error or transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Create Subscription Product Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/create-product Create a subscription product with a billing period and multi-currency pricing ``` POST /v1/actions/subscription-product/create-product ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | --------------- | -------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | | `storeId` | string | Yes | Store ID (`STO_xxx` format) | | `name` | string | Yes | Product name (max 64 chars) | | `billingPeriod` | string | Yes | `weekly`, `monthly`, `quarterly`, or `yearly` | | `prices` | object | Yes | Multi-currency pricing (see [Price Format](/api-reference/endpoints/onetime-products#price-object-format)) | | `description` | string \| null | No | Product description (supports Markdown; pass `null` or `""` to clear) | | `media` | array | No | Product images/videos (see [Media Format](/api-reference/endpoints/onetime-products#media-item-format)) | | `successUrl` | string \| null | No | Redirect URL after successful subscription (max 512 chars, must be a valid http(s) URL; pass `null` or `""` to clear) | | `metadata` | object | No | Custom key-value data. May include `trialDays` (integer, 1-365) to offer a free trial period | ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake, BillingPeriod, TaxCategory } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { product } = await client.subscriptionProducts.create({ storeId: "STO_2D5F8G3H1K4M6N9P", name: "Pro Plan", billingPeriod: BillingPeriod.Monthly, prices: { USD: { amount: "29.00", taxIncluded: false, taxCategory: TaxCategory.SaaS }, EUR: { amount: "27.00", taxIncluded: false, taxCategory: TaxCategory.SaaS }, }, description: "Full access to all Pro features.", successUrl: "https://example.com/welcome", metadata: { trialDays: 14 }, }); ``` ```bash cURL theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/subscription-product/create-product \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: MER_7K3M9P2R5T8W1Y4Z" \ -H "X-Timestamp: 2026-03-30T10:00:00Z" \ -H "X-Signature: BASE64_RSA_SIGNATURE" \ -d '{ "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan", "billingPeriod": "monthly", "prices": { "USD": { "amount": "29.00", "taxIncluded": false, "taxCategory": "saas" }, "EUR": { "amount": "27.00", "taxIncluded": false, "taxCategory": "saas" } }, "description": "Full access to all Pro features.", "successUrl": "https://example.com/welcome", "metadata": { "trialDays": 14 } }' ``` ```python Python theme={"system"} import requests # Assumes you have a sign_request() helper for RSA-SHA256 signing headers = sign_request("POST", "/v1/actions/subscription-product/create-product", body) body = { "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan", "billingPeriod": "monthly", "prices": { "USD": {"amount": "29.00", "taxIncluded": False, "taxCategory": "saas"}, "EUR": {"amount": "27.00", "taxIncluded": False, "taxCategory": "saas"}, }, "description": "Full access to all Pro features.", "successUrl": "https://example.com/welcome", "metadata": {"trialDays": 14}, } response = requests.post( "https://api.waffo.ai/v1/actions/subscription-product/create-product", json=body, headers=headers, ) data = response.json() ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3F7H2J5L8N1Q4S6U", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan", "description": "Full access to all Pro features.", "billingPeriod": "monthly", "prices": { "USD": { "amount": "29.00", "taxCategory": "saas" }, "EUR": { "amount": "27.00", "taxCategory": "saas" } }, "media": [], "successUrl": "https://example.com/welcome", "metadata": { "trialDays": 14 }, "status": "active", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T10:30:00.000Z" } } } ``` ## Response Fields The response is wrapped in `data.product`. The product object is a flattened detail view combining product and version fields. | Field | Type | Description | | --------------- | -------------- | --------------------------------------------------------------- | | `id` | string | Product ID (`PROD_xxx`) | | `storeId` | string | Store ID (`STO_xxx`) | | `name` | string | Product name (from current version) | | `description` | string \| null | Product description (from current version) | | `billingPeriod` | string | Billing period: `weekly`, `monthly`, `quarterly`, or `yearly` | | `prices` | object | Multi-currency pricing map (see below) | | `media` | array | Media items (from current version) | | `successUrl` | string \| null | Success redirect URL (from current version) | | `metadata` | object | Custom metadata (from current version, may include `trialDays`) | | `status` | string | Status in the current environment: `active` or `inactive` | | `createdAt` | string | Product creation timestamp (ISO 8601) | | `updatedAt` | string | Product last update timestamp (ISO 8601) | **Response price object** (per currency): | Field | Type | Description | | ------------- | ------ | ------------------------------------------------ | | `amount` | string | Price as a display format string (e.g., "29.00") | | `taxCategory` | string | Tax category for tax calculation | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing header: x-context-merchant-id` | The merchant context header was not forwarded | Fix your SDK configuration | | 400 | `Missing or invalid header: x-context-environment` | Environment header is missing or not `test` / `prod` | Set `X-Environment` to `test` or `prod` | | 400 | `Missing required field: storeId` / `Missing required field: name` / `Missing required field: prices (must have at least one currency)` | A required body field was omitted | Add the missing field, resubmit | | 400 | `Expected format: STO_xxx, got "X"` | `storeId` is not a valid Short ID | Use a `STO_` prefixed Short ID | | 400 | `Invalid or missing billingPeriod` | `billingPeriod` is not one of `weekly` / `monthly` / `quarterly` / `yearly` | Use one of the four allowed values | | 400 | `Invalid currency code: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY")` | A key in `prices` is not a valid ISO 4217 code | Use 3 uppercase letters | | 400 | `Invalid amount for X: "Y". Must be a positive number string (e.g., "9.99", "1000")` | Amount string couldn't be parsed | Use a positive decimal string | | 401 | `Unauthorized` | Authentication failed | Verify API key, timestamp, and signature | | 404 | `Store not found` | The decoded `storeId` does not exist (foreign key violation) | Verify the store belongs to your merchant account | | 500 | `Internal server error` | Transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Delete Group Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/delete-group Permanently delete a product group Permanently delete a product group. Products within the group are not affected. ``` POST /v1/actions/subscription-product-group/delete-group ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | ---------------------- | | `id` | string | Yes | Group ID (UUID format) | ## Example Request ```typescript SDK theme={"system"} await client.subscriptionProductGroups.delete({ id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product-group/delete-group", { id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}"""; String response = callApi("POST", "/v1/actions/subscription-product-group/delete-group", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product-group/delete-group", { "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", } result, err := callAPI("POST", "/v1/actions/subscription-product-group/delete-group", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a" }); let result = call_api("POST", "/v1/actions/subscription-product-group/delete-group", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a\"}"; char *response = call_api("POST", "/v1/actions/subscription-product-group/delete-group", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"})"; std::string response = callApi("POST", "/v1/actions/subscription-product-group/delete-group", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/delete-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product-group/delete-group" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/delete-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product-group/delete-group" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` This is a **permanent hard delete**. The products within the group are not deleted -- only the grouping relationship is removed. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ---------------------------- | --------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 404 | `Group not found` | The group is already gone or never belonged to your store | Verify the `id` | | 500 | `Internal server error` | Internal error or transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Subscription Product Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/overview Create and manage subscription products and product groups Subscription products support recurring billing with configurable billing periods and multi-currency pricing. Products can be organized into groups for shared trial management and tiered pricing plans. ## Billing Periods | Period | Frequency | | ----------- | -------------------- | | `weekly` | Every 7 days | | `monthly` | Every calendar month | | `quarterly` | Every 3 months | | `yearly` | Every 12 months | ## Group Object Product groups organize related subscription products (e.g., Free, Pro, Enterprise plans) and enable shared trial management across products within a group. Each group exists per environment -- one row for test and one for production. ```json theme={"system"} { "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pricing Plans", "description": "Free, Pro, and Enterprise tiers", "rules": { "sharedTrial": true }, "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", "PROD_1C3E5G7J0L2N4Q6S" ], "environment": "test", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T10:30:00.000Z" } ``` Group IDs are in **UUID format**, not Short ID format. This is the only entity in the API that uses raw UUIDs. | Field | Type | Description | | ------------- | -------------- | ------------------------------------------- | | `id` | string | Group ID (UUID format) | | `storeId` | string | Store ID (`STO_xxx` format) | | `name` | string | Group name (unique per store + environment) | | `description` | string \| null | Group description | | `rules` | object | Group rules (see below) | | `productIds` | string\[] | List of product IDs (`PROD_xxx` format) | | `environment` | string | `test` or `prod` | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp | ### Rules | Field | Type | Default | Description | | ------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sharedTrial` | boolean | `false` | When `true`, trial usage is shared across all products in the group. Prevents customers from signing up for repeated free trials by switching between products in the same group. | ## Endpoints Create a subscription product with billing period and multi-currency pricing. Update a subscription product's content. Creates a new immutable version if content changed. Publish a subscription product from test to production (first-publish only). Activate or deactivate a subscription product. Create a product group to organize related subscription products. Update a product group's name, description, rules, or product list. Permanently delete a product group. Publish a product group from test to production (supports repeated UPSERT). # Publish Group Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/publish-group Publish a product group from test to production environment Publish a product group from test to production environment. Uses UPSERT behavior -- you can re-publish after making changes, unlike product publishing which only supports the first publish. ``` POST /v1/actions/subscription-product-group/publish-group ``` **Authentication:** API Key Do **not** include the `X-Environment` header for this endpoint. Publishing is always one-way from test to production. ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | -------------------------------------------------------- | | `id` | string | Yes | Group ID (UUID format, must be a test environment group) | ## Example Request ```typescript SDK theme={"system"} await client.subscriptionProductGroups.publish({ id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product-group/publish-group", { id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}"""; String response = callApi("POST", "/v1/actions/subscription-product-group/publish-group", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product-group/publish-group", { "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", } result, err := callAPI("POST", "/v1/actions/subscription-product-group/publish-group", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a" }); let result = call_api("POST", "/v1/actions/subscription-product-group/publish-group", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a\"}"; char *response = call_api("POST", "/v1/actions/subscription-product-group/publish-group", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"})"; std::string response = callApi("POST", "/v1/actions/subscription-product-group/publish-group", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/publish-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product-group/publish-group" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/publish-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product-group/publish-group" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` Unlike product publishing, group publishing supports **repeated UPSERT operations**. You can update a test group and re-publish it to production at any time. The production group will be created or updated to match the current test group. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------ | ------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 400 | `Can only publish test environment groups` | The supplied group is already in production (or otherwise not test) | Only test-environment groups are publishable | | 400 | `Cannot publish: product_ids is empty` | The group has no member products | Add at least one product via `update-group`, resubmit | | 404 | `Group not found` | No group exists for the supplied `id` | Verify the `id` belongs to your store | | 500 | `Internal server error` | Internal error or transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Publish Product Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/publish-product Publish a subscription product from test to production environment Publish a subscription product from test to production environment. This is a one-way, first-publish-only operation. ``` POST /v1/actions/subscription-product/publish-product ``` **Authentication:** API Key Do **not** include the `X-Environment` header for this endpoint. Publishing is always one-way from test to production. ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | ------------------------------ | | `id` | string | Yes | Product ID (`PROD_xxx` format) | ## Example Request ```typescript SDK theme={"system"} await client.subscriptionProducts.publish({ id: "PROD_3F7H2J5L8N1Q4S6U", }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product/publish-product", { id: "PROD_3F7H2J5L8N1Q4S6U", }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"PROD_3F7H2J5L8N1Q4S6U"}"""; String response = callApi("POST", "/v1/actions/subscription-product/publish-product", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product/publish-product", { "id": "PROD_3F7H2J5L8N1Q4S6U", }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3F7H2J5L8N1Q4S6U", } result, err := callAPI("POST", "/v1/actions/subscription-product/publish-product", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3F7H2J5L8N1Q4S6U" }); let result = call_api("POST", "/v1/actions/subscription-product/publish-product", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3F7H2J5L8N1Q4S6U\"}"; char *response = call_api("POST", "/v1/actions/subscription-product/publish-product", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"PROD_3F7H2J5L8N1Q4S6U"})"; std::string response = callApi("POST", "/v1/actions/subscription-product/publish-product", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3F7H2J5L8N1Q4S6U"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/publish-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product/publish-product" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3F7H2J5L8N1Q4S6U"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/publish-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product/publish-product" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3F7H2J5L8N1Q4S6U", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan", "description": "Full access to all Pro features.", "billingPeriod": "monthly", "prices": { "USD": { "amount": "29.00", "taxCategory": "saas" }, "EUR": { "amount": "27.00", "taxCategory": "saas" } }, "media": [], "successUrl": "https://example.com/welcome", "metadata": { "trialDays": 14 }, "status": "active", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T12:00:00.000Z" } } } ``` ## Response Fields Same as [Create Subscription Product response](/api-reference/endpoints/subscription-products/create-product#response-fields). Only the **first publish** is supported. Once a product has a production version, this endpoint returns an error. To update a published product, use the [Update Product](/api-reference/endpoints/subscription-products/update-product) endpoint in the production environment. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------ | ------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | 400 | `Missing header: x-context-merchant-id` | The merchant context header was not forwarded | Fix your SDK configuration | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 400 | `Expected format: PROD_xxx, got "X"` | `id` is not a valid Short ID | Use a `PROD_` prefixed Short ID | | 400 | `No test version found` | The product has no version in the test environment to publish | Create and activate a test version first via Update Product / Update Status | | 400 | `Test version is not active` | Test version exists but its status is `inactive` | Activate the test version via Update Status, then publish | | 400 | `Already published to production` | Product already has a production version (publish is first-publish-only) | Use Update Product in the production environment instead | | 401 | `Unauthorized` | Authentication failed | Verify API key, timestamp, and signature | | 404 | `Product not found` / `Source version not found` | Product ID does not exist (or its source version is missing) | Verify the product ID | | 500 | `Internal server error` | Transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Group Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/update-group Update a product group's name, description, rules, or product list Update a product group's name, description, rules, or product list. ``` POST /v1/actions/subscription-product-group/update-group ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | ------------- | --------- | -------- | ----------------------------------------------------------------------- | | `id` | string | Yes | Group ID (UUID format) | | `name` | string | No | Updated group name | | `description` | string | No | Updated description | | `rules` | object | No | Updated rules (`{ sharedTrial: boolean }`) | | `productIds` | string\[] | No | Updated product list (`PROD_xxx` format). **Replaces the entire list.** | ## Example Request ```typescript SDK theme={"system"} const { group } = await client.subscriptionProductGroups.update({ id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", name: "Updated Pricing Plans", productIds: [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", ], }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product-group/update-group", { id: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", name: "Updated Pricing Plans", productIds: [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", ], }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a","name":"Updated Pricing Plans","productIds":["PROD_3F7H2J5L8N1Q4S6U","PROD_8B4D6F9H2K5M7P1R"]}"""; String response = callApi("POST", "/v1/actions/subscription-product-group/update-group", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product-group/update-group", { "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "name": "Updated Pricing Plans", "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", ], }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "name": "Updated Pricing Plans", "productIds": []string{ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R", }, } result, err := callAPI("POST", "/v1/actions/subscription-product-group/update-group", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "name": "Updated Pricing Plans", "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R" ] }); let result = call_api("POST", "/v1/actions/subscription-product-group/update-group", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a\",\"name\":\"Updated Pricing Plans\",\"productIds\":[\"PROD_3F7H2J5L8N1Q4S6U\",\"PROD_8B4D6F9H2K5M7P1R\"]}"; char *response = call_api("POST", "/v1/actions/subscription-product-group/update-group", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a","name":"Updated Pricing Plans","productIds":["PROD_3F7H2J5L8N1Q4S6U","PROD_8B4D6F9H2K5M7P1R"]})"; std::string response = callApi("POST", "/v1/actions/subscription-product-group/update-group", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "name": "Updated Pricing Plans", "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R" ] }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/update-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product-group/update-group" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f9a", "name": "Updated Pricing Plans", "productIds": [ "PROD_3F7H2J5L8N1Q4S6U", "PROD_8B4D6F9H2K5M7P1R" ] }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product-group/update-group $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product-group/update-group" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` The `productIds` field **replaces the entire product list**. To add a product, include all existing product IDs plus the new one. To remove a product, omit it from the list. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 400 | `Expected format: PROD_xxx, got "..."` | A `productIds` entry could not be decoded as a Short ID | Fix the ID, resubmit | | 404 | `Group not found` | No group exists for the supplied `id` | Verify the `id` belongs to your store | | 500 | `Internal server error` | Internal error or transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Subscription Product Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/update-product Update a subscription product's content with automatic immutable versioning Update a subscription product's content. If content has changed, a new immutable version is created automatically. If the content is identical to the current version, no new version is created. ``` POST /v1/actions/subscription-product/update-product ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | --------------- | -------------- | -------- | ----------------------------------------------------------------------------------------------- | | `id` | string | Yes | Product ID (`PROD_xxx` format) | | `name` | string | No | Updated product name (max 64 chars) | | `billingPeriod` | string | No | `weekly`, `monthly`, `quarterly`, or `yearly` | | `prices` | object | No | Updated multi-currency pricing | | `description` | string \| null | No | Updated description (pass `null` or `""` to clear) | | `media` | array | No | Updated media | | `successUrl` | string \| null | No | Updated redirect URL (max 512 chars, must be a valid http(s) URL; pass `null` or `""` to clear) | | `metadata` | object | No | Updated metadata (may include `trialDays`) | ## Example Request ```typescript SDK theme={"system"} const { product } = await client.subscriptionProducts.update({ id: "PROD_3F7H2J5L8N1Q4S6U", name: "Pro Plan v2", billingPeriod: BillingPeriod.Monthly, prices: { USD: { amount: "39.00", taxIncluded: false, taxCategory: TaxCategory.SaaS }, EUR: { amount: "36.00", taxIncluded: false, taxCategory: TaxCategory.SaaS }, }, metadata: { trialDays: 7 }, }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product/update-product", { id: "PROD_3F7H2J5L8N1Q4S6U", name: "Pro Plan v2", billingPeriod: "monthly", prices: { USD: { amount: "39.00", taxIncluded: false, taxCategory: "saas" }, EUR: { amount: "36.00", taxIncluded: false, taxCategory: "saas" }, }, metadata: { trialDays: 7 }, }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"PROD_3F7H2J5L8N1Q4S6U","name":"Pro Plan v2","billingPeriod":"monthly","prices":{"USD":{"amount":"39.00","taxIncluded":false,"taxCategory":"saas"},"EUR":{"amount":"36.00","taxIncluded":false,"taxCategory":"saas"}},"metadata":{"trialDays":7}}"""; String response = callApi("POST", "/v1/actions/subscription-product/update-product", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product/update-product", { "id": "PROD_3F7H2J5L8N1Q4S6U", "name": "Pro Plan v2", "billingPeriod": "monthly", "prices": { "USD": {"amount": "39.00", "taxIncluded": False, "taxCategory": "saas"}, "EUR": {"amount": "36.00", "taxIncluded": False, "taxCategory": "saas"}, }, "metadata": {"trialDays": 7}, }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3F7H2J5L8N1Q4S6U", "name": "Pro Plan v2", "billingPeriod": "monthly", "prices": map[string]interface{}{ "USD": map[string]interface{}{"amount": "39.00", "taxIncluded": false, "taxCategory": "saas"}, "EUR": map[string]interface{}{"amount": "36.00", "taxIncluded": false, "taxCategory": "saas"}, }, "metadata": map[string]interface{}{"trialDays": 7}, } result, err := callAPI("POST", "/v1/actions/subscription-product/update-product", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3F7H2J5L8N1Q4S6U", "name": "Pro Plan v2", "billingPeriod": "monthly", "prices": { "USD": {"amount": "39.00", "taxIncluded": false, "taxCategory": "saas"}, "EUR": {"amount": "36.00", "taxIncluded": false, "taxCategory": "saas"} }, "metadata": {"trialDays": 7} }); let result = call_api("POST", "/v1/actions/subscription-product/update-product", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3F7H2J5L8N1Q4S6U\",\"name\":\"Pro Plan v2\",\"billingPeriod\":\"monthly\",\"prices\":{\"USD\":{\"amount\":\"39.00\",\"taxIncluded\":false,\"taxCategory\":\"saas\"},\"EUR\":{\"amount\":\"36.00\",\"taxIncluded\":false,\"taxCategory\":\"saas\"}},\"metadata\":{\"trialDays\":7}}"; char *response = call_api("POST", "/v1/actions/subscription-product/update-product", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"PROD_3F7H2J5L8N1Q4S6U","name":"Pro Plan v2","billingPeriod":"monthly","prices":{"USD":{"amount":"39.00","taxIncluded":false,"taxCategory":"saas"},"EUR":{"amount":"36.00","taxIncluded":false,"taxCategory":"saas"}},"metadata":{"trialDays":7}})"; std::string response = callApi("POST", "/v1/actions/subscription-product/update-product", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "PROD_3F7H2J5L8N1Q4S6U", "name": "Pro Plan v2", "billingPeriod": "monthly", "prices": { "USD": { "amount": "39.00", "taxIncluded": false, "taxCategory": "saas" }, "EUR": { "amount": "36.00", "taxIncluded": false, "taxCategory": "saas" } }, "metadata": { "trialDays": 7 } }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/update-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product/update-product" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{ "id": "PROD_3F7H2J5L8N1Q4S6U", "name": "Pro Plan v2", "billingPeriod": "monthly", "prices": { "USD": { "amount": "39.00", "taxIncluded": false, "taxCategory": "saas" }, "EUR": { "amount": "36.00", "taxIncluded": false, "taxCategory": "saas" } }, "metadata": { "trialDays": 7 } }' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/update-product $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product/update-product" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3F7H2J5L8N1Q4S6U", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan v2", "description": null, "billingPeriod": "monthly", "prices": { "USD": { "amount": "39.00", "taxCategory": "saas" }, "EUR": { "amount": "36.00", "taxCategory": "saas" } }, "media": [], "successUrl": null, "metadata": { "trialDays": 7 }, "status": "active", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T11:00:00.000Z" } } } ``` ## Response Fields Same as [Create Subscription Product response](/api-reference/endpoints/subscription-products/create-product#response-fields). Product updates create **new immutable versions**. Existing subscriptions retain their original version. New signups use the latest version. If the submitted content is identical to the current version, no new version is created. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | 400 | `Missing header: x-context-merchant-id` | The merchant context header was not forwarded | Fix your SDK configuration | | 400 | `Missing or invalid header: x-context-environment` | Environment header is missing or not `test` / `prod` | Set `X-Environment` to `test` or `prod` | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 400 | `Expected format: PROD_xxx, got "X"` | `id` is not a valid Short ID | Use a `PROD_` prefixed Short ID | | 400 | `Field name must be a non-empty string` | `name` was provided as an empty string | Omit `name` to keep current value, or provide a non-empty string | | 400 | `Invalid billingPeriod` | `billingPeriod` is not one of `weekly` / `monthly` / `quarterly` / `yearly` | Use one of the four allowed values | | 400 | `Field prices must be a non-empty object` | `prices` was provided but empty | Omit `prices` to keep current value, or include at least one currency | | 400 | `Invalid currency code: "X". Must be 3 uppercase letters (e.g., "USD", "EUR", "JPY")` | A key in `prices` is not a valid ISO 4217 code | Use 3 uppercase letters | | 400 | `Invalid amount for X: "Y". Must be a positive number string (e.g., "9.99", "1000")` | Amount string couldn't be parsed | Use a positive decimal string | | 400 | Message contains `has no version` | The target environment (`test` or `prod`) has no version yet | Create a version in this environment first (publish from test, or create in test) | | 401 | `Unauthorized` | Authentication failed | Verify API key, timestamp, and signature | | 404 | `Product not found` / `Current version not found` | Product ID does not exist (or current version missing) | Verify the product ID | | 500 | `Internal server error` | Transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Status Source: https://docs.waffo.ai/api-reference/endpoints/subscription-products/update-status Activate or deactivate a subscription product Activate or deactivate a subscription product. ``` POST /v1/actions/subscription-product/update-status ``` **Authentication:** API Key ## Request Body | Field | Type | Required | Description | | -------- | ------ | -------- | ------------------------------ | | `id` | string | Yes | Product ID (`PROD_xxx` format) | | `status` | string | Yes | `active` or `inactive` | ## Example Request ```typescript SDK theme={"system"} import { ProductVersionStatus } from "@waffo/pancake-ts"; await client.subscriptionProducts.updateStatus({ id: "PROD_3F7H2J5L8N1Q4S6U", status: ProductVersionStatus.Inactive, }); ``` ```typescript TypeScript (fetch) theme={"system"} // Uses callApi() helper from authentication.mdx const result = await callApi("POST", "/v1/actions/subscription-product/update-status", { id: "PROD_3F7H2J5L8N1Q4S6U", status: "inactive", }); console.log(result.data); ``` ```java Java theme={"system"} // Uses callApi() helper from authentication.mdx String body = """ {"id":"PROD_3F7H2J5L8N1Q4S6U","status":"inactive"}"""; String response = callApi("POST", "/v1/actions/subscription-product/update-status", body); System.out.println(response); ``` ```python Python theme={"system"} # Uses call_api() helper from authentication.mdx result = call_api("POST", "/v1/actions/subscription-product/update-status", { "id": "PROD_3F7H2J5L8N1Q4S6U", "status": "inactive", }) print(result["data"]) ``` ```go Go theme={"system"} // Uses callAPI() helper from authentication.mdx body := map[string]interface{}{ "id": "PROD_3F7H2J5L8N1Q4S6U", "status": "inactive", } result, err := callAPI("POST", "/v1/actions/subscription-product/update-status", body) if err != nil { log.Fatal(err) } fmt.Println(string(result)) ``` ```rust Rust theme={"system"} // Uses call_api() helper from authentication.mdx let body = serde_json::json!({ "id": "PROD_3F7H2J5L8N1Q4S6U", "status": "inactive" }); let result = call_api("POST", "/v1/actions/subscription-product/update-status", &body).await?; println!("{}", result); ``` ```c C theme={"system"} /* Uses call_api() helper from authentication.mdx */ const char *body = "{\"id\":\"PROD_3F7H2J5L8N1Q4S6U\",\"status\":\"inactive\"}"; char *response = call_api("POST", "/v1/actions/subscription-product/update-status", body); printf("%s\n", response); free(response); ``` ```cpp C++ theme={"system"} // Uses callApi() helper from authentication.mdx std::string body = R"({"id":"PROD_3F7H2J5L8N1Q4S6U","status":"inactive"})"; std::string response = callApi("POST", "/v1/actions/subscription-product/update-status", body); std::cout << response << std::endl; ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3F7H2J5L8N1Q4S6U","status":"inactive"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/update-status $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-product/update-status" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"id":"PROD_3F7H2J5L8N1Q4S6U","status":"inactive"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL_REQUEST="POST /v1/actions/subscription-product/update-status $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL_REQUEST" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- "https://api.waffo.ai/v1/actions/subscription-product/update-status" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ --post-data="$BODY" ``` ## Success Response (200) ```json theme={"system"} { "data": { "product": { "id": "PROD_3F7H2J5L8N1Q4S6U", "storeId": "STO_2D5F8G3H1K4M6N9P", "name": "Pro Plan", "description": "Full access to all Pro features.", "billingPeriod": "monthly", "prices": { "USD": { "amount": "29.00", "taxCategory": "saas" }, "EUR": { "amount": "27.00", "taxCategory": "saas" } }, "media": [], "successUrl": "https://example.com/welcome", "metadata": { "trialDays": 14 }, "status": "inactive", "createdAt": "2026-03-30T10:30:00.000Z", "updatedAt": "2026-03-30T13:00:00.000Z" } } } ``` ## Response Fields Same as [Create Subscription Product response](/api-reference/endpoints/subscription-products/create-product#response-fields). Deactivating a subscription product prevents **new signups** but does **not** cancel existing active subscriptions. Subscribers continue their current billing cycle unaffected. ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------- | | 400 | `Missing header: x-context-merchant-id` | The merchant context header was not forwarded | Fix your SDK configuration | | 400 | `Missing or invalid header: x-context-environment` | Environment header is missing or not `test` / `prod` | Set `X-Environment` to `test` or `prod` | | 400 | `Missing required field: id` | Request body did not include `id` | Add `id`, resubmit | | 400 | `Expected format: PROD_xxx, got "X"` | `id` is not a valid Short ID | Use a `PROD_` prefixed Short ID | | 400 | `Invalid or missing status (must be 'active' or 'inactive')` | `status` is missing or not one of the two allowed values | Use `active` or `inactive` | | 400 | Message contains `has no version` | The target environment (`test` or `prod`) has no version yet | Create a version in this environment first (publish from test, or create in test) | | 401 | `Unauthorized` | Authentication failed | Verify API key, timestamp, and signature | | 404 | `Product not found` / `Current version not found` | Product ID does not exist (or current version missing) | Verify the product ID | | 500 | `Internal server error` | Transient downstream failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Cancel Subscription Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/cancel-subscription Cancel an active or pending subscription Cancel an active or pending subscription. The behavior depends on the current subscription status. ``` POST /v1/actions/subscription-order/cancel-order ``` **Authentication:** API Key (merchant, owner role) This endpoint also has a customer-side flow with session-token auth: see [Cancel Subscription (Customer)](/api-reference/endpoints/subscriptions/cancel-subscription-customer). ## Cancellation Behavior | Current Status | Action | Result Status | | -------------- | ------------------------------ | --------------------------------------- | | `pending` | Immediate cancel | `canceled` | | `active` | Cancel at period end (via PSP) | `canceling` -> `canceled` at period end | * **pending**: Directly canceled, status becomes `canceled` * **active**: PSP cancellation is triggered (takes effect at billing period end). Local status becomes `canceling`, then updated to `canceled` via Webhook when the period ends ## Request Body | Field | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------- | | `orderId` | string | Yes | Subscription order ID (Short ID format `ORD_xxx`) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const result = await client.orders.cancelSubscription({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(result.orderId); // "ORD_2aUyqjCzEIiEcYMKj7TZtw" console.log(result.status); // "canceling" or "canceled" ``` ```typescript TypeScript (Manual) theme={"system"} // Assumes callApi() is defined as shown in the Authentication guide const result = await callApi("POST", "/v1/actions/subscription-order/cancel-order", { orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }); ``` ```java Java theme={"system"} // Assumes callApi() is defined as shown in the Authentication guide String result = callApi("POST", "/v1/actions/subscription-order/cancel-order", "{\"orderId\":\"ORD_2aUyqjCzEIiEcYMKj7TZtw\"}"); ``` ```python Python theme={"system"} # Assumes call_api() is defined as shown in the Authentication guide result = call_api("POST", "/v1/actions/subscription-order/cancel-order", { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", }) ``` ```go Go theme={"system"} // Assumes callAPI() is defined as shown in the Authentication guide result, err := callAPI("POST", "/v1/actions/subscription-order/cancel-order", `{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}`) ``` ```rust Rust theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide let result = call_api("POST", "/v1/actions/subscription-order/cancel-order", r#"{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}"# ).await?; ``` ```c C theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide call_api("/v1/actions/subscription-order/cancel-order", "{\"orderId\":\"ORD_2aUyqjCzEIiEcYMKj7TZtw\"}"); ``` ```cpp C++ theme={"system"} // Assumes call_api() is defined as shown in the Authentication guide auto result = call_api("/v1/actions/subscription-order/cancel-order", R"({"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"})"); ``` ```bash cURL theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL="POST /v1/actions/subscription-order/cancel-order $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/cancel-order" \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $MERCHANT_ID" \ -H "X-Timestamp: $TIMESTAMP" \ -H "X-Signature: $SIGNATURE" \ -d "$BODY" ``` ```bash wget theme={"system"} MERCHANT_ID="MER_2aUyqjCzEIiEcYMKj7TZtw" TIMESTAMP=$(date +%s) BODY='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' BODY_HASH=$(echo -n "$BODY" | openssl dgst -sha256 -binary | base64 -w 0) CANONICAL="POST /v1/actions/subscription-order/cancel-order $TIMESTAMP $BODY_HASH" SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -sign private_key.pem | base64 -w 0) wget -qO- --post-data="$BODY" \ --header="Content-Type: application/json" \ --header="X-Merchant-Id: $MERCHANT_ID" \ --header="X-Timestamp: $TIMESTAMP" \ --header="X-Signature: $SIGNATURE" \ "https://api.waffo.ai/v1/actions/subscription-order/cancel-order" ``` ## Success Response (200) -- Active Subscription ```json theme={"system"} { "data": { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", "status": "canceling" } } ``` ## Success Response (200) -- Pending Subscription ```json theme={"system"} { "data": { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", "status": "canceled" } } ``` ### Response Fields | Field | Type | Description | | --------- | ------ | -------------------------------------------- | | `orderId` | string | Order ID (Short ID) | | `status` | string | New order status (`canceling` or `canceled`) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing X-Context-Merchant-Id header` | Merchant context missing in API Key auth | Verify the auth pipeline | | 400 | `Missing required field: orderId` | `orderId` was not provided in the body | Fix the request body, then resubmit | | 400 | `Expected format: ORD_xxx, got "..."` | `orderId` Short ID could not be decoded | Fix the `orderId` format, then resubmit | | 400 | `Subscription cannot be canceled, current status: X` | Order status is not `pending` or `active` (e.g. already `canceled`, `canceling`, `expired`) | The subscription is no longer cancellable | | 401 | `Authentication failed` | Invalid API Key signature | Verify auth headers | | 403 | `Order does not belong to user` | Ownership check failed | Verify the caller owns the order | | 404 | `Order not found` | Order does not exist | Verify the order ID | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | | 502 | `Failed to cancel subscription` | Local update or PSP cancellation failed | Retry with exponential backoff (start 5s, max 3 attempts) | # Cancel Subscription (Customer) Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/cancel-subscription-customer Customer-side cancellation of a subscription using a session token This is the customer-token call path of the same endpoint described in [Cancel Subscription (Merchant API Key)](/api-reference/endpoints/subscriptions/cancel-subscription). Behavior, request body, and response shape are identical — only the auth surface and the call site differ. ``` POST /v1/actions/subscription-order/cancel-order ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer or buyer role) The `orderId` must belong to the customer that the session token was minted for. Tokens minted for one buyer cannot cancel another buyer's subscription. ## Cancellation Behavior | Current Status | Action | Result Status | | -------------- | ------------------------------ | -------------------------------------- | | `pending` | Immediate cancel | `canceled` | | `active` | Cancel at period end (via PSP) | `canceling` → `canceled` at period end | ## Request Body | Field | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------- | | `orderId` | string | Yes | Subscription order ID (Short ID format `ORD_xxx`) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.cancelSubscription({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(result.orderId); // "ORD_2aUyqjCzEIiEcYMKj7TZtw" console.log(result.status); // "canceling" or "canceled" ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/subscription-order/cancel-order", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/cancel-order" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' \ "https://api.waffo.ai/v1/actions/subscription-order/cancel-order" ``` ## Success Response (200) ```json theme={"system"} { "data": { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", "status": "canceling" } } ``` ### Response Fields | Field | Type | Description | | --------- | ------ | -------------------------------------------- | | `orderId` | string | Order ID (Short ID) | | `status` | string | New order status (`canceling` or `canceled`) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: orderId` | `orderId` was not provided in the body | Fix the body, resubmit | | 400 | `Expected format: ORD_xxx, got "..."` | `orderId` Short ID could not be decoded | Fix the `orderId` format, resubmit | | 400 | `Subscription cannot be canceled, current status: X` | Order status is not `pending` or `active` (e.g. already `canceled`, `canceling`, `expired`) | The subscription is no longer cancellable | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Order does not belong to user` | The token's buyer is not the order owner | Mint a token for the correct buyer | | 404 | `Order not found` | Order does not exist | Verify the order ID | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | | 502 | `Failed to cancel subscription` | Local update or PSP cancellation failed | Retry with exponential backoff (start 5s, max 3 attempts) | # Change Subscription Product Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/change-product Swap the product on an existing subscription order (Coming Soon) Swap the product attached to an active subscription order. ``` POST /v1/actions/subscription-order/change-product ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer role) **Coming Soon** — This endpoint is not yet implemented and currently returns `501 Not Implemented`. The request signature below is provisional and may change before GA. While not implemented, the route remains locked: calling it will not mutate any subscription state. ## Request Body | Field | Type | Required | Description | | ----------------- | ------ | -------- | --------------------------------------------------------- | | `orderId` | string | Yes | Current subscription order ID (Short ID format `ORD_xxx`) | | `targetProductId` | string | Yes | Target product ID (Short ID format `PROD_xxx`) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.changeSubscriptionProduct({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", targetProductId: "PROD_36ZqlJPatGOsjz7AtYqAwj", }); ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/subscription-order/change-product", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", targetProductId: "PROD_36ZqlJPatGOsjz7AtYqAwj", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/change-product" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw","targetProductId":"PROD_36ZqlJPatGOsjz7AtYqAwj"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw","targetProductId":"PROD_36ZqlJPatGOsjz7AtYqAwj"}' \ "https://api.waffo.ai/v1/actions/subscription-order/change-product" ``` ## Current Response (501) ```json theme={"system"} { "data": null, "errors": [ { "message": "Not implemented", "layer": "order" } ] } ``` ### Response Fields (planned) | Field | Type | Description | | --------- | ------ | ------------------------------------- | | `orderId` | string | Subscription order ID (Short ID) | | `status` | string | Order status after the product change | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ----------------------------------------- | ---------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: orderId` | `orderId` was not provided in the body | Fix the request body, then resubmit | | 400 | `Missing required field: targetProductId` | `targetProductId` was not provided in the body | Fix the request body, then resubmit | | 400 | `Expected format: ORD_xxx, got "..."` | `orderId` Short ID could not be decoded | Fix the `orderId` format, then resubmit | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Order does not belong to user` | Ownership check failed | Verify the caller owns the order | | 404 | `Order not found` | Order does not exist | Verify the order ID | | 501 | `Not implemented` | Endpoint is not yet available | Wait for GA — do not retry | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Create Subscription Order Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/create-subscription-order Create a subscription order from a checkout session and obtain the PSP checkout URL Create a subscription order from a pre-built checkout session. Returns the PSP checkout URL — redirect the buyer there to complete payment and start the subscription. ``` POST /v1/actions/subscription-order/create-order ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer or shopper role) ## Behavior * **Price snapshot** — The subscription price is frozen at order creation (`price_snapshot`); two-phase pricing (`regularPhase` + optional `specialPhase` for trials) is supported. * **Billing period mapping** — `weekly` -> `week/1`, `monthly` -> `month/1`, `quarterly` -> `month/3`, `yearly` -> `year/1`. * **PSP fault tolerance** — If the PSP call fails, the local order is canceled and the error is surfaced. * **Session pre-fill** — `billingDetail` and `buyerEmail` from the request body override any pre-filled values on the session; if both are missing the request is rejected with 400. * **Email normalization** — `buyerEmail` is normalized with `trim().toLowerCase()` before use. The following fields are sourced from the session and must NOT be sent in the request body: `storeId`, `productId`, `billingPeriod`, `currency`. They are ignored if provided. ## Request Body | Field | Type | Required | Description | | ---------------------------- | ------- | -------------------------------- | ------------------------------------------------------------- | | `checkoutSessionId` | string | Yes | Checkout session ID returned by `create-checkout-session` | | `billingDetail` | object | Conditional | Buyer billing info; required if not pre-filled on the session | | `billingDetail.country` | string | Yes (if `billingDetail` present) | ISO 3166-1 alpha-2 country code | | `billingDetail.isBusiness` | boolean | Yes (if `billingDetail` present) | Whether the buyer is a business | | `billingDetail.postcode` | string | No | Postal code | | `billingDetail.state` | string | Required for US/CA | State / province code | | `billingDetail.businessName` | string | Required if `isBusiness=true` | Legal business name | | `billingDetail.taxId` | string | Required for EU business buyers | VAT / tax ID (e.g. `DE123456789`) | | `buyerEmail` | string | Conditional | Buyer email; required if not pre-filled on the session | | `buyerIp` | string | No | Buyer IP address (used for tax calculation) | | `successUrl` | string | No | Post-checkout redirect URL; overrides the session-level value | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.createSubscriptionOrder({ checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", billingDetail: { country: "DE", isBusiness: true, businessName: "Acme GmbH", taxId: "DE123456789", }, buyerEmail: "customer@example.com", successUrl: "https://myapp.com/subscription/success", }); console.log(result.checkoutUrl); ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/subscription-order/create-order", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ checkoutSessionId: "cs_550e8400-e29b-41d4-a716-446655440000", billingDetail: { country: "DE", isBusiness: true, businessName: "Acme GmbH", taxId: "DE123456789", }, buyerEmail: "customer@example.com", successUrl: "https://myapp.com/subscription/success", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/create-order" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","billingDetail":{"country":"DE","isBusiness":true,"businessName":"Acme GmbH","taxId":"DE123456789"},"buyerEmail":"customer@example.com","successUrl":"https://myapp.com/subscription/success"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"checkoutSessionId":"cs_550e8400-e29b-41d4-a716-446655440000","billingDetail":{"country":"DE","isBusiness":true,"businessName":"Acme GmbH","taxId":"DE123456789"},"buyerEmail":"customer@example.com","successUrl":"https://myapp.com/subscription/success"}' \ "https://api.waffo.ai/v1/actions/subscription-order/create-order" ``` ## Success Response (200) ```json theme={"system"} { "data": { "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_xxx" } } ``` ### Response Fields | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------- | | `checkoutUrl` | string | PSP checkout URL — redirect the buyer here to complete payment | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Invalid JSON body` | Request body is not valid JSON | Fix the request body, then resubmit | | 400 | `Missing required field: checkoutSessionId` | `checkoutSessionId` was not provided | Fix the request body, then resubmit | | 400 | `Session product type mismatch: expected subscription` | The session was created for a non-subscription product | Use a session created for a subscription product | | 400 | `Environment mismatch between request and session` | Request `X-Environment` does not match the session environment | Align the request environment with the session | | 400 | `Session missing subscription information (productVersionId or billingPeriod)` | Session is malformed — missing required subscription fields | Recreate the checkout session | | 400 | `Missing billingDetail: provide in request body or pre-fill in checkout session` | Neither the body nor the session contains `billingDetail` | Provide `billingDetail` in the body, then resubmit | | 400 | `State is required for US/CA` | `billingDetail.state` is required for US / CA buyers | Add `billingDetail.state`, then resubmit | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Session does not belong to this store` | The session belongs to a different store | Verify the session's store ownership | | 409 | `Checkout session invalid, please re-enter checkout` | Session does not exist or has expired | Re-create the checkout session | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Subscription Order Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/overview Create and manage subscription orders Subscription order endpoints allow you to create recurring billing orders and manage their lifecycle. Subscriptions are created through checkout sessions (with `productType: "subscription"`) and managed via cancellation and status transitions. ## Subscription Status Values | Status | Description | | ----------- | ----------------------------------------------- | | `pending` | Subscription created, awaiting first payment | | `active` | Active and billing normally | | `trialing` | In free trial period | | `canceling` | Cancellation requested, active until period end | | `past_due` | Payment failed, retrying | | `canceled` | Subscription ended, no further billing | | `expired` | Subscription expired | ## Endpoints Create a subscription order using a checkout session. Cancel an active or pending subscription. ## Coming Soon The following endpoints are planned but not yet implemented (currently return `501 Not Implemented`): * **Change Product** (`POST /v1/actions/subscription-order/change-product`) -- Upgrade or downgrade a subscription to a different product. * **Reactivate Subscription** (`POST /v1/actions/subscription-order/reactivate-order`) -- Reactivate a canceled subscription. # Reactivate Subscription Source: https://docs.waffo.ai/api-reference/endpoints/subscriptions/reactivate-order Reactivate a subscription that is canceling at the end of the current period Reactivate a subscription that is currently in `canceling` state. The pending end-of-period cancellation is removed and the subscription continues to renew as scheduled. ``` POST /v1/actions/subscription-order/reactivate-order ``` **Authentication:** Session Token — see [Customer Endpoints](/api-reference/endpoints/auth/customer-endpoints) (customer or buyer role) ## Reactivation Behavior | Current Status | Action | Result Status | | --------------------------------------------- | --------------------------------------------------- | ------------- | | `canceling` | Remove pending end-of-period cancellation (via PSP) | `active` | | `canceled` | Not allowed — already fully canceled | — | | `pending` / `active` / `past_due` / `expired` | Not allowed — only `canceling` is reactivatable | — | * Only subscriptions in `canceling` (cancellation scheduled at period end) can be reactivated. * Fully canceled (`canceled`) subscriptions cannot be reactivated — start a new subscription instead. * No additional charge — the current billing period continues, and renewal resumes at its original schedule. ## Request Body | Field | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------- | | `orderId` | string | Yes | Subscription order ID (Short ID format `ORD_xxx`) | ## Example Request ```typescript TypeScript (SDK) theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ sessionToken: window.WAFFO_SESSION_TOKEN, // injected by the merchant's portal environment: "prod", }); const result = await client.orders.reactivateSubscription({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }); console.log(result.orderId); // "ORD_2aUyqjCzEIiEcYMKj7TZtw" console.log(result.status); // "active" ``` ```typescript TypeScript (Manual) theme={"system"} const result = await fetch("https://api.waffo.ai/v1/actions/subscription-order/reactivate-order", { method: "POST", headers: { "Authorization": `Bearer ${SESSION_TOKEN}`, "Content-Type": "application/json", "X-Environment": "prod", }, body: JSON.stringify({ orderId: "ORD_2aUyqjCzEIiEcYMKj7TZtw", }), }).then(r => r.json()); ``` ```bash cURL theme={"system"} curl -X POST "https://api.waffo.ai/v1/actions/subscription-order/reactivate-order" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Environment: prod" \ -d '{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' ``` ```bash wget theme={"system"} wget -qO- \ --header="Authorization: Bearer $SESSION_TOKEN" \ --header="Content-Type: application/json" \ --header="X-Environment: prod" \ --post-data='{"orderId":"ORD_2aUyqjCzEIiEcYMKj7TZtw"}' \ "https://api.waffo.ai/v1/actions/subscription-order/reactivate-order" ``` ## Success Response (200) ```json theme={"system"} { "data": { "orderId": "ORD_2aUyqjCzEIiEcYMKj7TZtw", "status": "active" } } ``` ### Response Fields | Field | Type | Description | | --------- | ------ | --------------------------- | | `orderId` | string | Order ID (Short ID) | | `status` | string | New order status (`active`) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: orderId` | `orderId` was not provided in the body | Fix the request body, then resubmit | | 400 | `Expected format: ORD_xxx, got "..."` | `orderId` Short ID could not be decoded | Fix the `orderId` format, then resubmit | | 400 | `Only canceling subscriptions can be reactivated` | Order status is not `canceling` (e.g. `canceled`, `active`, `expired`) | The subscription is not reactivatable | | 401 | `Authentication failed` | Session token invalid, expired, or malformed | Re-mint the session token via Issue Session Token | | 403 | `Order does not belong to user` | Ownership check failed | Verify the caller owns the order | | 404 | `Order not found` | Order does not exist | Verify the order ID | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Add Webhook Source: https://docs.waffo.ai/api-reference/endpoints/webhooks/add-webhook Configure a new webhook endpoint for a store Add a webhook endpoint to a store. Each store can have up to **20 webhooks** across all channels. ``` POST /v1/actions/store/add-webhook ``` **Authentication:** API Key (owner or admin role required) ## Request Body | Field | Type | Required | Description | | ---------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `storeId` | string | Yes | Store ID (Short ID format `STO_xxx`) | | `channel` | string | Yes | One of `http`, `feishu`, `discord`, `telegram`, `slack` | | `url` | string | Yes | Target webhook URL (HTTPS; merchant ensures URL matches the chosen channel) | | `events` | string\[] | Yes | Subscribed event types — e.g. `["order.completed", "refund.succeeded"]`. Empty array means no events fire to this webhook. | | `testMode` | boolean | Yes | `true` = fires for test transactions; `false` = fires for production | | `secret` | string \| null | No | Channel-specific credential (e.g. Telegram `chat_id`). Stored as opaque text. | ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); // Standard HTTPS webhook (RSA-signed envelope) const { webhook } = await client.webhooks.add({ storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw", channel: "http", url: "https://example.com/webhooks/pancake", events: ["order.completed", "refund.succeeded"], testMode: false, }); ``` ```typescript SDK (Discord) theme={"system"} // Discord webhook — uses Discord's native embed format await client.webhooks.add({ storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw", channel: "discord", url: "https://discord.com/api/webhooks/123456789/abc-def-ghi", events: ["order.completed"], testMode: false, }); ``` ```typescript SDK (Telegram) theme={"system"} // Telegram bot — chat_id goes in secret await client.webhooks.add({ storeId: "STO_2aUyqjCzEIiEcYMKj7TZtw", channel: "telegram", url: "https://api.telegram.org/bot123456:ABC-DEF/sendMessage", events: ["order.completed", "refund.failed"], testMode: false, secret: "8737101383", }); ``` ```bash cURL theme={"system"} curl -X POST https://api.waffo.com/v1/actions/store/add-webhook \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $WAFFO_MERCHANT_ID" \ -H "X-Signature: ..." \ -d '{ "storeId": "STO_2aUyqjCzEIiEcYMKj7TZtw", "channel": "http", "url": "https://example.com/webhooks/pancake", "events": ["order.completed"], "testMode": false }' ``` ## Success Response (200) ```json theme={"system"} { "data": { "webhook": { "id": "11111111-2222-3333-4444-555555555555", "storeId": "uuid-of-store", "channel": "http", "url": "https://example.com/webhooks/pancake", "events": ["order.completed", "refund.succeeded"], "testMode": false, "secret": null, "createdAt": "2026-05-07T00:00:00.000Z", "updatedAt": "2026-05-07T00:00:00.000Z" } } } ``` The returned `webhook.id` is a UUID, not a Short ID — webhook IDs are not in the `IdPrefix` scope. Pass it as-is to `update-webhook` and `remove-webhook`. ## Response Fields | Field | Type | Description | | ----------- | -------------- | --------------------------------------------------------------------- | | `id` | string | Webhook UUID | | `storeId` | string | Owning store UUID | | `channel` | string | Webhook channel (`http`, `feishu`, `discord`, `telegram`, or `slack`) | | `url` | string | Target webhook URL | | `events` | string\[] | Subscribed event types | | `testMode` | boolean | `true` for test transactions, `false` for production | | `secret` | string \| null | Channel-specific credential (opaque text), `null` if unset | | `createdAt` | string | Creation timestamp (ISO 8601) | | `updatedAt` | string | Last update timestamp (ISO 8601) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | ------------------------------------------------------------------------ | ------------------------------------------------- | --------------------------------------------------------- | | 400 | `Missing required field: storeId` | `storeId` was not provided | Fix the request body, resubmit | | 400 | `Expected format: STO_xxx, got "..."` | `storeId` Short ID could not be decoded | Fix the `storeId` format, resubmit | | 400 | `testMode must be a boolean` | `testMode` is not a boolean | Pass a boolean value, resubmit | | 400 | `Invalid channel: must be one of http, feishu, discord, telegram, slack` | `channel` is not in the allowed list | Use one of the allowed channels | | 400 | `Invalid URL format` | `url` is not a valid URL | Fix the URL, resubmit | | 400 | `events must be a string array` | `events` is not an array of strings | Pass a string array, resubmit | | 400 | `secret must be a string or null` | `secret` is not string or null | Pass string or null, resubmit | | 400 | `Webhook limit reached (max 20 per store)` | Store already has 20 webhooks | Remove an existing webhook first | | 401 | `Missing merchantId in request context` | API Key authentication did not resolve a merchant | Verify API Key headers and signature | | 403 | `Not authorized to manage webhooks for this store` | Merchant is not `owner` or `admin` of the store | Verify the merchant's role on this store | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Webhook Endpoints Source: https://docs.waffo.ai/api-reference/endpoints/webhooks/overview Manage webhook endpoints for HTTP, Feishu, Discord, Telegram, and Slack delivery Pancake supports multiple webhook endpoints per store across five channels: standard HTTPS (RSA-signed envelope), Feishu, Discord, Telegram, and Slack. Each endpoint is an independent record in `store.store_webhooks` with its own URL, subscribed event list, and `test`/`prod` environment binding. ## Channels | Channel | Payload format | Signature | Notes | | ---------- | --------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------------- | | `http` | JSON envelope `{ id, eventType, eventId, storeId, mode, data }` | RSA-SHA256 (`X-Waffo-Signature` header) | Default channel; preserves the existing webhook contract. | | `feishu` | Feishu interactive card | None — URL token | Chinese title text, UTC+8 timestamp | | `discord` | Discord embed | None — URL token | English text, client-local timestamp | | `telegram` | `sendMessage` payload (`text` HTML, `chat_id`) | None — bot token in URL | English text, UTC; `chat_id` stored in `secret` | | `slack` | Attachments payload | None — incoming-webhook URL token | English text, UTC | ## URL host Any HTTPS URL is accepted regardless of channel. The merchant is responsible for matching the URL to the channel's expected platform — if a Discord URL is configured under `channel: "slack"`, the dispatcher will encode the payload in Slack's attachments format and send it to the Discord endpoint, which will respond with an error and the failure is recorded in `webhook_deliveries`. ## Multiple webhooks per URL The same URL can be configured under multiple webhooks within a store — webhooks fan out, so different event subscriptions, channels, or `testMode` flags can share a URL. Each webhook delivers independently, which means consumers receiving identical payloads on a shared URL must dedupe by `(eventType, eventId)`. ## Limits Each store may have at most **20 webhooks** across all channels. Exceeding the limit returns 400. ## Listing webhooks There is no `list-webhooks` endpoint — querying the configured webhook list goes through GraphQL `Store.storeWebhooks` (filtered automatically by environment via `test_mode`): ```graphql theme={"system"} query GetStoreWebhooks($storeId: String!) { store(id: $storeId) { storeWebhooks { id channel url events testMode secret createdAt } } } ``` ## Endpoints Configure a new webhook endpoint for a store. Change a webhook's URL, events, or secret. Hard-delete a webhook (history retained). # Remove Webhook Source: https://docs.waffo.ai/api-reference/endpoints/webhooks/remove-webhook Hard-delete a webhook (history retained) Remove a webhook from a store. This is a **hard delete** — the row is removed from `store.store_webhooks` immediately, with no soft-delete flag. Historical `webhook_deliveries` records are retained for audit (the `storeWebhookId` foreign key is set to `null`). ``` POST /v1/actions/store/remove-webhook ``` **Authentication:** API Key (owner or admin role required) ## Request Body | Field | Type | Required | Description | | ----- | ------ | -------- | ------------ | | `id` | string | Yes | Webhook UUID | ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { webhook } = await client.webhooks.remove({ id: "11111111-2222-3333-4444-555555555555", }); // webhook contains a snapshot of the row before deletion ``` ```bash cURL theme={"system"} curl -X POST https://api.waffo.com/v1/actions/store/remove-webhook \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $WAFFO_MERCHANT_ID" \ -H "X-Signature: ..." \ -d '{ "id": "11111111-2222-3333-4444-555555555555" }' ``` ## Success Response (200) Returns the webhook entity as it existed immediately before deletion (so the caller can confirm what was removed). Same shape as [`add-webhook`](/api-reference/endpoints/webhooks/add-webhook#success-response-200). ## Response Fields | Field | Type | Description | | ----------- | -------------- | ---------------------------------------------------- | | `id` | string | Webhook UUID | | `storeId` | string | Owning store UUID | | `channel` | string | Webhook channel | | `url` | string | Target webhook URL | | `events` | string\[] | Subscribed event types | | `testMode` | boolean | `true` for test transactions, `false` for production | | `secret` | string \| null | Channel-specific credential, `null` if unset | | `createdAt` | string | Creation timestamp (ISO 8601) | | `updatedAt` | string | Last update timestamp (ISO 8601) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | | 400 | `Missing required field: id` | `id` was not provided | Fix the request body, resubmit | | 400 | `id must be a valid UUID` | `id` is not a valid UUID | Fix the `id`, resubmit | | 403 | `Not authorized to manage webhooks for this store` | The caller's merchant role on the owning store is not `owner` or `admin` | Switch to an API Key whose merchant has the required role | | 404 | `Webhook not found` | No webhook matches the supplied `id` (or already removed) | The webhook is already gone — treat as success and reconcile local state | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Update Webhook Source: https://docs.waffo.ai/api-reference/endpoints/webhooks/update-webhook Update a webhook's URL, events, or secret Update an existing webhook's URL, subscribed events, or channel-specific secret. The `channel` and `testMode` fields are immutable — to change them, remove and re-add the webhook. ``` POST /v1/actions/store/update-webhook ``` **Authentication:** API Key (owner or admin role required) ## Request Body | Field | Type | Required | Description | | -------- | -------------- | -------- | --------------------------------------------------------------------------- | | `id` | string | Yes | Webhook UUID (returned from `add-webhook` or GraphQL `Store.storeWebhooks`) | | `url` | string | No | Replace target URL. | | `events` | string\[] | No | Replace subscribed event types | | `secret` | string \| null | No | Replace channel-specific credential. Pass `null` to clear. | `channel` is permanent for a given webhook record — to switch channel, remove and re-add. URL changes are accepted as-is; merchant ensures the new URL matches the channel. ## Example Request ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); // Add more events to an existing webhook await client.webhooks.update({ id: "11111111-2222-3333-4444-555555555555", events: ["order.completed", "refund.succeeded", "subscription.canceled"], }); ``` ```bash cURL theme={"system"} curl -X POST https://api.waffo.com/v1/actions/store/update-webhook \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: $WAFFO_MERCHANT_ID" \ -H "X-Signature: ..." \ -d '{ "id": "11111111-2222-3333-4444-555555555555", "events": ["order.completed", "refund.succeeded"] }' ``` ## Success Response (200) Returns the updated webhook entity. Identical shape to [`add-webhook`](/api-reference/endpoints/webhooks/add-webhook#success-response-200). ## Response Fields | Field | Type | Description | | ----------- | -------------- | ---------------------------------------------------------------- | | `id` | string | Webhook UUID | | `storeId` | string | Owning store UUID | | `channel` | string | Webhook channel (immutable) | | `url` | string | Target webhook URL | | `events` | string\[] | Subscribed event types | | `testMode` | boolean | `true` for test transactions, `false` for production (immutable) | | `secret` | string \| null | Channel-specific credential, `null` if cleared | | `createdAt` | string | Creation timestamp (ISO 8601) | | `updatedAt` | string | Last update timestamp (ISO 8601) | ## Errors > **Retry policy:** Never retry 4xx — fix the request and resubmit. Retry 5xx with exponential backoff (start 5s, max 3 attempts). | Status | `errors[0].message` | What it means | Recommended handling | | ------ | -------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | | 400 | `Missing required field: id` | `id` was not provided | Fix the request body, resubmit | | 400 | `id must be a valid UUID` | `id` is not a valid UUID | Fix the `id`, resubmit | | 400 | `Invalid URL format` | `url` could not be parsed as a valid URL | Fix the URL format (HTTPS, well-formed), resubmit | | 400 | `events must be a string array` | `events` is not a JSON array of strings | Send `events` as a string array (e.g. `["order.completed"]`) | | 400 | `secret must be a string or null` | `secret` is neither a string nor `null` | Send a string value, or `null` to clear | | 403 | `Not authorized to manage webhooks for this store` | The caller's merchant role on the owning store is not `owner` or `admin` | Switch to an API Key whose merchant has the required role | | 404 | `Webhook not found` | No webhook matches the supplied `id` (or deleted by a concurrent request) | Verify the `id`; if just removed, re-list webhooks before retrying | | 500 | `Internal server error` | Unexpected server-side failure | Retry with exponential backoff (start 5s, max 3 attempts) | # Errors Source: https://docs.waffo.ai/api-reference/errors Handle API errors gracefully ## Error Format All error responses follow a consistent structure: ```json theme={"system"} { "data": null, "errors": [ { "message": "Missing required field: name", "layer": "product" } ] } ``` ### Reading the Errors Array The `errors` array is ordered from root cause to top-level caller: * `errors[0]` -- the **root cause** of the failure (most useful for debugging) * `errors[n]` -- the top-level caller that surfaced the error In most cases, you only need to inspect `errors[0].message` for the actionable error description. *** ## HTTP Status Codes | Code | Meaning | Description | | ---- | --------------- | ---------------------------------------------- | | 200 | Success | Request processed successfully | | 400 | Bad Request | Invalid parameters or malformed request body | | 401 | Unauthorized | Missing or invalid authentication credentials | | 403 | Forbidden | Valid credentials but insufficient permissions | | 404 | Not Found | Requested resource does not exist | | 409 | Conflict | Idempotent request is already being processed | | 429 | Rate Limited | Too many requests in a short period | | 500 | Server Error | Internal server error | | 501 | Not Implemented | Requested feature is not yet available | | 502 | Bad Gateway | Upstream service error | *** ## Error `layer` Field Each error includes a `layer` string indicating which part of the system produced the error. Use the `layer` value alongside `message` to identify the root cause when debugging. *** ## Common Error Scenarios ### Authentication Errors (401) ```json theme={"system"} { "data": null, "errors": [ { "message": "Token has expired", "layer": "user" } ] } ``` **Solutions:** * Verify your Merchant ID and private key are correct * Ensure the SDK is initialized with valid credentials * Check that your private key matches the registered public key ### Validation Errors (400) ```json theme={"system"} { "data": null, "errors": [ { "message": "Store name must be between 1 and 100 characters", "layer": "store" } ] } ``` **Solutions:** * Check required fields are present * Verify field value formats and constraints * Ensure amounts are display format strings (e.g., "29.00") ### Permission Errors (403) ```json theme={"system"} { "data": null, "errors": [ { "message": "Only store owner can delete store", "layer": "store" } ] } ``` **Solutions:** * Verify the API Key has the required permissions * Ensure the user belongs to the correct store ### Idempotency Conflicts (409) ```json theme={"system"} { "data": null, "errors": [ { "message": "Request with this idempotency key is already being processed", "layer": "gateway" } ] } ``` **Solutions:** * Wait for the original request to complete * Use a different `X-Idempotency-Key` for a new request *** ## Handling Errors in Code ### TypeScript SDK ```typescript theme={"system"} import { WaffoPancake, WaffoPancakeError } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); try { const { store } = await client.stores.create({ name: "My Store" }); } catch (err) { if (err instanceof WaffoPancakeError) { const rootCause = err.errors[0]; console.error(`Error [${rootCause.layer}]: ${rootCause.message}`); // err.status contains the HTTP status code } } ``` *** ## Retry Strategy Retry these status codes with exponential backoff: * **429** -- Rate limited (respect `Retry-After` header if present) * **500** -- Internal server error * **502** -- Bad gateway ```javascript theme={"system"} async function retryRequest(fn, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { const status = error.status; if (![429, 500, 502].includes(status) || attempt === maxRetries - 1) { throw error; } const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s await new Promise(resolve => setTimeout(resolve, delay)); } } } ``` Do not retry `400`, `401`, `403`, or `404` errors. These indicate issues that must be fixed in the request itself. *** ## Idempotency for Safe Retries Use the `X-Idempotency-Key` header to safely retry write operations without creating duplicates: ```bash theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "X-Store-Slug: your-store-slug" \ -H "X-Environment: test" \ -H "X-Idempotency-Key: order-abc-123" \ -d '{"productId": "...", "productType": "onetime", "currency": "USD"}' ``` * Idempotency keys are cached for **24 hours** * Sending the same key returns the cached response * If the original request is still processing, returns `409 Conflict` # API Reference Source: https://docs.waffo.ai/api-reference/introduction Integrate Waffo Pancake into your application ## Overview The Waffo Pancake API lets you programmatically manage your entire payment infrastructure: * Create and manage stores * Create products (one-time and subscription) * Generate checkout sessions and process orders * Manage subscriptions and billing * Query data via GraphQL * Handle refunds ## Base URL All API requests are made to: ``` https://api.waffo.ai/v1 ``` ## Architecture The API uses a hybrid approach: * **REST endpoints** (`/v1/actions/...`) for all write operations (create, update, delete) * **GraphQL** (`/v1/graphql`) for all read operations (queries) All REST endpoints use `POST` method exclusively. There are no GET, PUT, PATCH, or DELETE methods. ## TypeScript SDK The official [`@waffo/pancake-ts`](https://www.npmjs.com/package/@waffo/pancake-ts) SDK wraps the entire API with full type safety. It handles authentication, request signing, idempotency keys, and webhook verification automatically. ```bash theme={"system"} npm install @waffo/pancake-ts ``` ```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!, }); ``` Every endpoint documented below includes an SDK example alongside the REST/cURL examples. [Full SDK documentation ->](/integrate/sdks) *** ## Authentication Waffo Pancake uses **API Key authentication** for all programmatic API access. API Key authentication is handled automatically by the SDK. For public-facing checkout flows, use **Store Slug** authentication with the `X-Store-Slug` header. [Learn more about authentication ->](/api-reference/authentication) *** ## Common Headers | Header | Required | Description | | ------------------- | ----------- | ------------------------------------------------------------------------ | | `Content-Type` | Yes | Always `application/json` | | `X-Store-Slug` | Conditional | Store slug (for public checkout flows) | | `X-Environment` | Conditional | `test` or `prod` (required with Store Slug auth, not needed for API Key) | | `X-Idempotency-Key` | Optional | Unique ID for write operations (cached 24h) | API Key authentication headers (`X-Merchant-Id`, `X-Timestamp`, `X-Signature`) are handled automatically by the SDK. You only need to provide your Merchant ID and private key when initializing the client. *** ## Request Format * **Method**: All write endpoints use `POST` * **Body**: JSON * **Timestamps**: ISO 8601 UTC (e.g., `2026-01-23T00:00:00.000Z`) * **Amounts**: Display format strings (e.g., `"29.00"` = \$29.00 USD) * **Currencies**: ISO 4217 codes (e.g., `USD`, `EUR`, `JPY`) * **Status values**: Always lowercase (e.g., `active`, not `ACTIVE`) ### ID Formats All externally-facing entity IDs use **Short ID** format: `{PREFIX}_{base62}`. | Prefix | Entity | Example | | ------ | ----------------- | ----------------------------- | | `MER` | Merchant | `MER_2D5F8G3H1K4M6N9P` | | `STO` | Store | `STO_3bVzrkD0FJjFdZNLk8Ualx` | | `PROD` | Product / Version | `PROD_4cWAslE1GKkGeaOMl9Vbmy` | | `ORD` | Order | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `PAY` | Payment | `PAY_6eYCunG3IMmIgcQOnaXdoA` | | `REF` | Refund | `REF_7fZDvoH4JNnJhdRPobYepB` | | `TKT` | Ticket | `TKT_8gAEwpI5KOoKieSQL1ZfqC` | | `KEY` | API Key | `KEY_4F7H0I5J3K6M8N1P` | Checkout Session IDs use a special format: `cs_` + UUID (e.g., `cs_550e8400-e29b-41d4-a716-446655440000`). They are not part of the Short ID system. *** ## Response Format ### Success ```json theme={"system"} { "data": { "store": { "id": "STO_3bVzrkD0FJjFdZNLk8Ualx", "name": "My Store", "status": "active", "createdAt": "2026-01-15T10:30:00.000Z" } } } ``` ### Error ```json theme={"system"} { "data": null, "errors": [ { "message": "Store slug already exists", "layer": "store" } ] } ``` In the `errors` array, `errors[0]` is the **root cause** of the failure. Subsequent entries represent higher-level callers in the request chain. ### Error `layer` Field Each error includes a `layer` string indicating which part of the system produced the error. Use this to identify the root cause when debugging. The value is always one of the predefined layer names (e.g., `"gateway"`, `"store"`, `"product"`). *** ## HTTP Status Codes | Code | Description | | ---- | -------------------------------------------------- | | 200 | Success | | 400 | Bad Request -- invalid parameters | | 401 | Unauthorized -- authentication failed | | 403 | Forbidden -- insufficient permissions | | 404 | Not Found | | 409 | Conflict -- idempotent request already in progress | | 429 | Rate Limited -- too many requests | | 500 | Internal Server Error | | 501 | Not Implemented | | 502 | Bad Gateway | *** ## Environments API Key authentication determines the environment automatically based on which key verifies successfully. Store Slug authentication requires the `X-Environment` header: | Environment | Header Value | Description | | ----------- | --------------------- | ------------------------------ | | Test | `X-Environment: test` | No real charges, isolated data | | Production | `X-Environment: prod` | Real transactions | *** ## Idempotency Prevent duplicate write operations by including an `X-Idempotency-Key` header: | Item | Specification | | -------------- | -------------------------------------------------- | | Header | `X-Idempotency-Key` | | Max length | 256 characters | | Allowed chars | Letters, numbers, hyphens (`-`), underscores (`_`) | | Cache duration | 24 hours | | Scenario | Behavior | Status | | ------------------------- | -------------------------------------------- | -------- | | First request | Executes normally, caches 2xx response | Original | | Duplicate (completed) | Returns cached response without re-executing | Original | | Duplicate (in progress) | Returns conflict error | 409 | | Original failed (non-2xx) | Same key can be retried | -- | *** ## Endpoint Groups Issue session tokens for checkout flows Create, update, and delete stores Create and manage one-time purchase products Create tiered subscription products and groups Create checkout sessions and orders Manage subscription lifecycle Request and process refunds Query all data with GraphQL # Payment Error Codes Source: https://docs.waffo.ai/api-reference/payment-error-codes Reference for every payment failure reason surfaced in the Waffo dashboard When a transaction fails, the **Error code** link in the dashboard's transaction details jumps to the matching entry here. 19 error codes 26 error codes 23 error codes 5 error codes 1 error codes ## Customer needs to fix input *Your customer needs to correct something on the checkout (card details, CVV, address, OTP, etc.) and retry.*
Code Meaning Why it failed Recommended action
CARD\_ABNORMAL\_CARDNUMBER Invalid card number **Cause:** The card number entered is invalid or does not exist Ask the customer to re-enter it.
CARD\_ABNORMAL\_NOTACTIVATED Card not yet activated **Cause:** The customer's card hasn't been activated Ask them to activate it with their bank.
CARD\_ABNORMAL\_NOTACTIVATED\_ACCOUNT Bank account not activated **Cause:** The customer's bank account is inactive Ask them to contact their bank.
CARD\_AMOUNTOVERLIMIT Exceeds card transaction limit **Cause:** Amount exceeds the card's per-transaction or daily limit Customer may split the charge or use another card.
CARD\_AUTHENTICATION\_FAILED Card details verification failed **Cause:** Couldn't initiate bank verification with the card details Ask the customer to recheck number, expiry, and CVV.
CARD\_AUTHENTICATION\_FAILED\_ADDRESS Billing address mismatch **Cause:** The billing address doesn't match the issuer's records Ask the customer to correct it.
CARD\_AUTHENTICATION\_FAILED\_CVV2 Incorrect CVV **Cause:** The card's security code is wrong Ask the customer to re-enter it.
CARD\_AUTHENTICATION\_FAILED\_NEED\_ADDITIONAL\_AUTH Additional verification required **Cause:** The bank requires extra verification The customer should contact their bank or complete verification on retry.
CARD\_AUTHENTICATION\_FAILED\_VERIFICATIONDATA Verification data mismatch **Cause:** Information provided during verification didn't match the issuer's records Ask the customer to retry.
CARD\_AUTHENTICATION\_FAILED\_VERIFICATIONDATA 3-D Secure data mismatch **Cause:** Information provided during 3-D Secure didn't match the issuer's records Ask the customer to retry.
CARD\_AUTHENTICATION\_FAILED\_VERIFICATIONDATA One-time code incorrect **Cause:** The customer's one-time verification code from the bank was wrong Ask them to request a new code and retry.
CARD\_BANK\_REJECT\_ISSUER\_CONTACT Issuer requires customer contact **Cause:** The customer's bank wants them to call to verify or lift a restriction before retrying Ask the customer to contact their bank.
CARD\_BANK\_REJECT\_ISSUER\_URGENT Bank flagged abnormality **Cause:** The customer's bank flagged something and wants them to call immediately Ask the customer to contact their bank right away.
CARD\_OVERLIMIT Exceeds bank limit **Cause:** Amount exceeds the customer's credit limit or per-transaction cap Suggest splitting the charge or using another card.
CARD\_PIN\_INVALID PIN or CVV incorrect **Cause:** The PIN or CVV entered was wrong Ask the customer to re-verify.
EXCEED\_DAILY\_LIMIT Daily limit reached **Cause:** The customer's daily spending hit the bank's cap Suggest retrying tomorrow or using another card.
INITIATE\_AUTHENTICATION\_FAILED Verification could not start **Cause:** Couldn't start bank verification with this card number Ask the customer to recheck the card number.
USER\_CPF\_INVALID Brazilian Tax ID (CPF) invalid **Cause:** The Brazilian Tax ID the customer entered is malformed or doesn't exist Ask them to correct it.
CREATE\_ORDER\_EXPIRED\_TOKEN Checkout page expired **Cause:** The customer took too long on the checkout page Ask them to start a new checkout.
## Customer needs a new payment method *This card can't be used. Ask the customer to use a different card or payment method.*
Code Meaning Why it failed Recommended action
CARD\_ABNORMAL Card in abnormal status **Cause:** The customer's card has been reported lost or frozen Ask them to use another payment method.
CARD\_ABNORMAL\_CLOSED Bank account closed **Cause:** The customer's bank account is closed Ask them to use another payment method.
CARD\_ABNORMAL\_CLOSED\_FROMACCOUNT Source account closed **Cause:** The funding account linked to the customer's card is closed Ask them to use another method.
CARD\_ABNORMAL\_EXPIRED Card expired **Cause:** The customer's card has expired Ask them to update it or use a different card.
CARD\_ABNORMAL\_FRAUD Issuer flagged as fraud **Cause:** The customer's bank blocked this on fraud suspicion Ask them to contact the bank or use another card.
CARD\_ABNORMAL\_ISSUER Issuer not registered **Cause:** The customer's bank isn't registered with the card network Ask them to use a different card.
CARD\_ABNORMAL\_LOST Card reported lost **Cause:** The customer's card was reported lost Ask them to use another payment method.
CARD\_ABNORMAL\_NOCREDIT No linked credit account **Cause:** Couldn't find a credit account tied to the customer's card Ask them to verify or use another card.
CARD\_ABNORMAL\_STOLEN Card reported stolen **Cause:** The customer's card has been reported lost or stolen Ask them to use another payment method.
CARD\_BANK\_REJECT\_BLOCK Bank account blocked **Cause:** The customer's bank account is frozen Ask them to use another method or contact the bank.
CARD\_BANK\_REJECT\_FRAUD Bank blocked for fraud **Cause:** The customer's bank flagged this transaction as fraud and blocked it Ask them to use another method.
CARD\_BANK\_REJECT\_LAW Account restricted by law **Cause:** The customer's account is legally restricted from transactions Ask them to use another method.
CARD\_BANK\_REJECT\_REVOKEALL\_ACCOUNT Account access revoked **Cause:** The customer's account access has been fully revoked Ask them to use another method.
CARD\_INSUFFICIENT\_BALANCE Insufficient funds **Cause:** The customer's card doesn't have enough funds Ask them to top up or use another card.
CARD\_INVALID\_TRANSACTION Card type not supported **Cause:** The customer's card doesn't support this transaction type Ask them to use a different card.
CARD\_PAYMENT\_RETRY\_LIMIT Retry limit reached **Cause:** Too many failed attempts on this card; the network has blocked further retries Ask the customer to use a different card.
CARD\_ABNORMAL\_NOCHECKING Card couldn't be verified **Cause:** The issuer flagged the customer's card as abnormal and couldn't verify it Ask them to use another card.
CARD\_ABNORMAL\_RESTRICTED Card restricted in this region **Cause:** The customer's card is restricted or locked in this region Ask them to use another method.
CARD\_NOTALLOWED\_TXN Transaction type not allowed **Cause:** The issuer prohibits this transaction type on this card (e.g. cross-border) Ask the customer to use a different card.
PAYMENT\_REJECTION Blocked by Waffo risk **Cause:** This transaction was blocked by Waffo's risk rules Contact support if you think this is wrong.
PAYMENT\_CHANNEL\_REJECTION Payment rejected **Cause:** The payment service rejected this transaction on its own risk rules Ask the customer to use another method.
PAYMENT\_CHANNEL\_REJECTION Subscription charge rejected **Cause:** The saved payment method for this subscription has not been verified Ask the customer to verify it in the customer portal.
PAYMENT\_CHANNEL\_REJECTION Subscription charge failed: card expired **Cause:** The card on file for this subscription has expired Ask the customer to update it in the customer portal.
PAYMENT\_CHANNEL\_REJECTION Invalid mobile number **Cause:** The customer's mobile number format is invalid Ask them to correct it.
CREATE\_ORDER\_FAILED Payment service under maintenance **Cause:** The payment service is undergoing maintenance Ask the customer to retry later.
CREATE\_ORDER\_FAILED Payment service declined **Cause:** The payment service rejected this transaction on its own risk rules Ask the customer to use another method.
## Transient issues — retry later *A temporary glitch in the bank, card network, or payment service. The customer can usually retry in a few minutes.*
Code Meaning Why it failed Recommended action
CARD\_AUTHENTICATION\_ERROR Verification protocol error **Cause:** A protocol or communication issue occurred during verification Ask the customer to retry shortly.
CARD\_BANK\_REJECT\_REVOKE\_SINGLE Bank revoked this transaction **Cause:** The customer's bank revoked this specific authorization Ask them to retry.
CARD\_BANK\_REJECT\_SECURITY Blocked by bank security **Cause:** The customer's bank blocked this for security Ask them to retry later or contact the bank.
CARD\_COUNTOVERLIMIT Too many attempts **Cause:** The customer's card has hit a frequency limit Ask them to retry later.
ISSUE\_BANK\_DECLINE Issuer declined (no reason) **Cause:** The customer's bank declined without specifying why Ask them to retry later.
RISK\_TRANSACTION\_LIMIT Waffo risk control triggered **Cause:** Waffo's amount or velocity rules blocked this transaction Ask the customer to retry later or use another card.
TIMEOUT\_CHANNEL\_CLOSE Payment service timed out **Cause:** The payment service didn't respond in time, so this attempt was auto-closed Ask the customer to retry.
CARD\_BANK\_REJECT Bank declined **Cause:** The customer's bank declined this without a specific reason Ask them to retry later.
CARD\_GENERAL\_ERROR Bank generic error **Cause:** The bank returned a non-specific error Ask the customer to retry or switch methods.
CARD\_ORG\_OTHER\_ERROR Card network error **Cause:** The card network (Visa, Mastercard, etc.) returned an internal error Ask the customer to retry.
CARD\_SYSTEMERROR Bank temporarily down **Cause:** The customer's bank is having transient issues Ask them to retry shortly.
CREATE\_ORDER\_ERROR Payment initiation timed out **Cause:** Network timeout when creating the transaction Ask the customer to retry.
PAYMENT\_CHANNEL\_ERROR Payment service communication error **Cause:** An unexpected error occurred communicating with the payment service Ask the customer to retry.
PAYMENT\_CHANNEL\_ERROR Payment service network error **Cause:** The network connection to the payment service failed Ask the customer to retry.
PAYMENT\_CHANNEL\_ERROR Payment service communication error **Cause:** An unexpected error occurred communicating with the payment service Ask the customer to retry.
PAYMENT\_CHANNEL\_ERROR Unexpected payment service response **Cause:** The payment service returned an unparseable response Ask the customer to retry.
PAYMENT\_FAILED Payment failed **Cause:** See the transaction detail for the specific reason Contact support if unclear.
PAYMENT\_FAILED Customer abandoned checkout **Cause:** The customer left the checkout page without finishing Reach out if you want to recover the sale.
PAYMENT\_FAILED Payment service notification failed **Cause:** The payment may have succeeded but the notification didn't arrive Check the order status again later.
PAYMENT\_FAILED Payment service error **Cause:** The payment service didn't give specific details Ask the customer to retry or switch methods.
SYSTEM\_ERROR Internal system error **Cause:** A transient internal error in Waffo Ask the customer to retry; contact support if persistent.
UNKNOWN\_ERROR Unknown error **Cause:** An undefined exception occurred Ask the customer to retry; contact support if it keeps happening.
UNSPECIFIED\_PAYMENT\_FAILURE Payment failed (no specific reason) **Cause:** The bank or payment service didn't give a specific reason Ask the customer to try another method.
## Terminal — no action available *The payment never went through and the transaction is closed. No remediation is possible; you can still reach out to the customer if you want to recover the sale.*
Code Meaning Why it failed Recommended action
CANCEL\_CLOSE Order closed **Cause:** This order was closed No action needed.
TIMEOUT\_CLOSE Customer payment timeout **Cause:** The customer didn't pay in time and the order was closed Send a new payment link if needed.
USER\_CANCEL\_CLOSE Customer canceled **Cause:** The customer canceled this order Reach out if you want to recover the sale.
CARD\_CARDHOLDER\_REJECT Customer declined at bank **Cause:** The customer canceled the payment at their bank Reach out if you want to recover the sale.
MERCHANT\_ORDER\_TIMEOUT Order expired **Cause:** The order's expiration passed without payment and was auto-closed Send a new payment link if needed.
## Other *Bank-side duplicate detection.*
Code Meaning Why it failed Recommended action
CARD\_BANK\_REJECT\_DUPLICATE Duplicate transaction **Cause:** The bank detected this as a duplicate and blocked it to prevent double-charging The customer should contact the bank if it isn't.
# Webhooks Source: https://docs.waffo.ai/api-reference/webhooks Receive real-time event notifications ## Overview Webhooks deliver real-time notifications to your server when events occur — orders, payments, subscriptions, and refunds. ``` Event occurs → Waffo Pancake → fan-out → Your channel(s) ``` A single store can have **multiple webhooks**, each delivering to a different channel. The available channels are: | Channel | Delivery target | Payload format | | ---------- | ---------------------------------- | ------------------------------- | | `http` | Your HTTPS endpoint | RSA-SHA256-signed JSON envelope | | `feishu` | Lark / Feishu bot incoming webhook | Interactive card (Lark format) | | `discord` | Discord channel webhook | Embed message (Discord format) | | `telegram` | Telegram bot `sendMessage` URL | HTML-formatted text | | `slack` | Slack incoming webhook | Attachment with mrkdwn fields | The `http` channel uses the JSON envelope and signature verification described on this page — most of this guide covers that channel. For the IM channels, payloads use each platform's native format and authentication is handled by the URL token; you don't need to verify signatures. **Using TypeScript?** The [`@waffo/pancake-ts`](https://www.npmjs.com/package/@waffo/pancake-ts) SDK has built-in public keys and auto-detects the environment — one line to verify the `http` channel. *** ## Setup * **HTTP** — build a server endpoint that accepts POST requests and returns `200`. * **Feishu / Discord / Telegram / Slack** — create a bot or incoming webhook in the target platform and copy its URL. For Telegram, also note the chat ID that should receive messages. Waffo uses one fixed key pair per environment — Test and Production — shared across all stores. You don't get a key back when registering a webhook; you read it from the Dashboard. Open the [Dashboard](https://pancake.waffo.ai/merchant/dashboard) → any store → **Settings → Webhooks**, and copy the **Webhook Public Key** for the environment you're integrating (Test or Production). Store it in your server's config; you'll use it to verify every incoming HTTP webhook. Every store's Dashboard shows the same Test key and the same Production key — they're platform-level. Adding, editing, or deleting webhook URLs does not change them. Add a webhook in **Dashboard → Settings → Webhooks**, or call [`POST /v1/actions/store/add-webhook`](/api-reference/endpoints/webhooks/add-webhook). Each webhook record specifies one channel, one URL, the subscribed events, and the target environment (`testMode: true` for Test, `false` for Production). You can register multiple webhooks per store. Use the Dashboard "Send Test Event" button to deliver a sample event to one or all of your registered webhooks. For the HTTP channel, use the code examples below to verify signatures before processing events. IM channels deliver pre-rendered messages and require no handling on your side. *** ## Environment Isolation Each webhook is registered for a single environment via the `testMode` flag. Test and Production are fully independent: | Aspect | Test | Production | | ----------------------------------- | ---------------------------- | ----------------------------------------- | | Webhook record | `testMode: true` | `testMode: false` | | Signing key (HTTP only) | Test key pair | Production key pair | | Verification public key (HTTP only) | Dashboard Test key | Dashboard Production key | | Selector when delivering | Header `X-Environment: test` | Header `X-Environment: prod` (or omitted) | The `mode` field in each HTTP payload indicates the source environment: `"test"` or `"prod"`. Always use the public key matching the event's `mode`. A Test key cannot verify Production events, and vice versa. *** ## Payload Format ### Headers | Header | Description | | ------------------- | ------------------------------------------------ | | `Content-Type` | `application/json` | | `X-Waffo-Signature` | Signature string: `t=,v1=` | | `X-Waffo-Event` | Event type (e.g., `order.completed`) | ### Body ```json theme={"system"} { "id": "PAY_6eYCunG3IMmIgcQOnaXdoA", "timestamp": "2026-03-10T08:30:00.000Z", "eventType": "order.completed", "eventId": "PAY_6eYCunG3IMmIgcQOnaXdoA", "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx", "storeName": "My Store", "mode": "prod", "data": { "orderId": "ORD_5dXBtmF2HLlHfbPNm0Wcnz", "orderStatus": "completed", "buyerEmail": "buyer@example.com", "currency": "USD", "amount": "29.00", "taxAmount": "2.90", "taxRate": 0.1, "taxName": "Consumption Tax", "subtotal": "26.10", "total": "29.00", "productName": "Pro Plan", "orderMetadata": { "planId": "pro" }, "orderMerchantExternalId": "ORDER-2026-00891", "productMetadata": {}, "paymentId": "PAY_6eYCunG3IMmIgcQOnaXdoA", "paymentStatus": "succeeded", "paymentMethod": "card", "paymentLast4": "4242", "paymentDate": "2026-03-10" } } ``` ### Top-level Fields | Field | Type | Description | | ----------- | ------ | --------------------------------------------------------------------------------------------------------------- | | `id` | string | Event entity ID — same as `eventId` for most events | | `timestamp` | string | Event time (ISO 8601 UTC) | | `eventType` | string | Event type (see [Event Types](#event-types)) | | `eventId` | string | Business event identifier — maps to different entities per event type (see [eventId Mapping](#eventid-mapping)) | | `storeId` | string | Store ID | | `storeName` | string | Store name | | `mode` | string | `"test"` or `"prod"` | ### `data` Fields The `data` object contains transaction details. Some fields are always present; others appear only for specific event types or when the data is available. **Always present:** | Field | Type | Description | | ----------------- | ------ | ----------------------------------------------------------------------------- | | `orderId` | string | Order ID | | `orderStatus` | string | Order status (e.g., `"completed"`, `"active"`, `"canceling"`) | | `buyerEmail` | string | Buyer email address | | `currency` | string | ISO 4217 currency code (e.g., `"USD"`, `"JPY"`) | | `amount` | string | Transaction amount including tax (display format, e.g., `"29.00"`) | | `taxAmount` | string | Tax amount (display format, e.g., `"2.90"`) | | `productName` | string | Product name | | `orderMetadata` | object | Order-level metadata from checkout session (merchant-defined key-value pairs) | | `productMetadata` | object | Product-level metadata set when creating/updating the product | **Included when available:** | Field | Type | Present when | Description | | -------------------------------- | ------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `merchantProvidedBuyerIdentity` | string | Set at checkout | Merchant's custom buyer identifier | | `orderMerchantExternalId` | string | Set at checkout | Order's merchant business-side identifier (set at checkout creation, max 128 chars). Present on order events and on `refund.*` events (inherited from the original order). | | `refundTicketMerchantExternalId` | string | Set on refund ticket creation | Refund ticket's merchant business-side identifier (max 128 chars). Only present on `refund.succeeded` / `refund.failed` events. | | `billingDetail` | object | Set at checkout | Billing address (`country`, `isBusiness`, etc.) | | `taxRate` | number | Tax applied | Tax rate as decimal (e.g., `0.1` for 10%) | | `taxName` | string | Tax applied | Tax name (e.g., `"Consumption Tax"`) | | `subtotal` | string | Available | Subtotal before tax (display format) | | `total` | string | Available | Total after tax (display format) | | `productDescription` | string | Set on product | Product description | **Payment events** (`order.completed`, `subscription.payment_succeeded`): | Field | Type | Description | | ---------------------- | ------ | -------------------------------------------------- | | `paymentId` | string | Payment ID | | `paymentStatus` | string | Payment status (`"succeeded"`, `"failed"`) | | `paymentMethod` | string | Payment method type (e.g., `"card"`) | | `paymentLast4` | string | Last 4 digits of payment instrument | | `paymentDate` | string | Payment date (ISO 8601 date, e.g., `"2026-03-10"`) | | `paymentFailureReason` | string | Failure reason (when payment failed) | **Subscription events** (`subscription.*`): | Field | Type | Description | | -------------------- | ------ | ------------------------------------------------------------------ | | `billingPeriod` | string | `"weekly"`, `"monthly"`, `"quarterly"`, `"yearly"` | | `currentPeriodStart` | string | Current billing period start (ISO 8601 date) | | `currentPeriodEnd` | string | Current billing period end (ISO 8601 date) | | `canceledAt` | string | Cancellation timestamp (ISO 8601, present when canceling/canceled) | **Refund events** (`refund.succeeded`, `refund.failed`): | Field | Type | Description | | -------------------------------- | ------ | --------------------------------------------------------------------------------------- | | `refundStatus` | string | `"succeeded"` or `"failed"` | | `refundReason` | string | Refund reason | | `refundCreatedAt` | string | Refund creation timestamp (ISO 8601) | | `orderMerchantExternalId` | string | Order's merchant business-side identifier (inherited from the original order, when set) | | `refundTicketMerchantExternalId` | string | Refund ticket's merchant business-side identifier (when set on ticket creation) | | `paymentId` | string | Original payment ID | | `paymentStatus` | string | Original payment status | | `paymentMethod` | string | Original payment method | | `paymentLast4` | string | Original payment last 4 digits | | `paymentDate` | string | Original payment date | Example `data` for a `refund.succeeded` event (only the refund-specific fields shown): ```json theme={"system"} { "refundStatus": "succeeded", "refundReason": "Customer requested refund", "refundCreatedAt": "2026-03-12T09:15:00.000Z", "orderMerchantExternalId": "ORDER-2026-00891", "refundTicketMerchantExternalId": "REF-2026-00891", "paymentId": "PAY_6eYCunG3IMmIgcQOnaXdoA", "paymentStatus": "succeeded", "paymentMethod": "card", "paymentLast4": "4242", "paymentDate": "2026-03-10" } ``` Amounts are **display format strings**, already converted from minor units. For example, USD `"29.00"` = 2900 cents; JPY `"4500"` = ¥4500. Use `subtotal` and `total` for itemized display when available. ### eventId Mapping The `eventId` identifies the business entity that triggered the event: | Event Type | eventId maps to | Example | | -------------------------------- | ---------------- | ------------------------------------ | | `order.completed` | Payment ID | `PAY_6eYCunG3IMmIgcQOnaXdoA` | | `subscription.activated` | Order ID | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `subscription.payment_succeeded` | Payment ID | `PAY_6eYCunG3IMmIgcQOnaXdoA` | | `subscription.canceling` | Order ID | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `subscription.uncanceled` | Order ID | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `subscription.updated` | Order ID | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `subscription.canceled` | Order ID | `ORD_5dXBtmF2HLlHfbPNm0Wcnz` | | `subscription.past_due` | Order ID + month | `ORD_5dXBtmF2HLlHfbPNm0Wcnz-2026-04` | | `refund.succeeded` | Refund ID | `REF_4cWAtlE1GKkGebONl9Xbnx` | | `refund.failed` | Refund ID | `REF_4cWAtlE1GKkGebONl9Xbnx` | `subscription.past_due` appends `-YYYY-MM` to the eventId. The same subscription triggers at most one `past_due` event per calendar month. If still overdue the next month, a new event fires. *** ## Event Types ### Overview | Event | Trigger | eventId | | -------------------------------- | ------------------------------------------------- | ---------------- | | `order.completed` | One-time order payment succeeded | Payment ID | | `subscription.activated` | Subscription first payment succeeded | Order ID | | `subscription.payment_succeeded` | Renewal payment succeeded (not first) | Payment ID | | `subscription.canceling` | Cancellation requested — active until period ends | Order ID | | `subscription.uncanceled` | Cancellation withdrawn | Order ID | | `subscription.updated` | Product changed (upgrade/downgrade) | Order ID | | `subscription.canceled` | Subscription terminated (period ended) | Order ID | | `subscription.past_due` | Renewal payment failed | Order ID + month | | `refund.succeeded` | Refund completed | Refund ID | | `refund.failed` | Refund failed | Refund ID | `subscription.uncanceled` and `subscription.updated` event templates are ready and will activate once the corresponding features launch. ### Event Details **Trigger**: One-time order payment succeeds for the first time. **Payload**: * `data.amount` — Payment amount (including tax) * `data.orderId` — The one-time order ID **Recommended actions**: * Deliver digital goods (license keys, download links, activation codes) * Update your order management system * Send buyer confirmation (if not using Waffo's built-in emails) The same order only triggers `order.completed` once. Refunds are notified via `refund.succeeded` / `refund.failed`. **Trigger**: First payment on a new subscription succeeds (`pending` → `active`). **Payload**: * `data.amount` — First payment amount (including tax) * `data.productName` — Subscription product name **Recommended actions**: * Provision the subscriber's account and grant access * Record the subscription start date Only fires when a subscription transitions from `pending` to `active` for the first time. Subsequent renewals use `subscription.payment_succeeded`. **Trigger**: A recurring renewal payment succeeds (not the first payment). **Payload**: * `data.amount` — This period's renewal amount (including tax) * `data.orderId` — The subscription order ID **Recommended actions**: * Extend the service period * Generate an invoice for this billing cycle * If the subscription was previously `past_due`, restore full access **Trigger**: Buyer or merchant requests cancellation. The subscription remains active until the current paid period ends. **Recommended actions**: * Show "Subscription expires on \[date]" notice * Offer a retention flow (e.g., discounted renewal) * **Do not** revoke access — the buyer has paid for the current period The buyer can withdraw the cancellation before the period ends (triggers `subscription.uncanceled`). **Trigger**: Cancellation is withdrawn before the current period ends. **Recommended actions**: * Remove the "expiring soon" notice * Restore auto-renewal status **Trigger**: Subscription product changes (upgrade or downgrade). **Payload**: * `data.productName` — New product name after the change * `data.amount` — New amount **Recommended actions**: * Update the buyer's access level (add/remove features) * Update billing records **Trigger**: Subscription is terminated — the paid period has ended and no further renewals will occur. **Recommended actions**: * Revoke access (or downgrade to a free tier) * Retain data for a grace period (in case the buyer re-subscribes) * Send a "subscription ended" confirmation This is a terminal state. The subscription is irreversibly ended. **Trigger**: Renewal payment fails and the subscription enters an overdue state. **Payload**: * `data.amount` — The amount due for this period * `eventId` — Format: `{orderId}-YYYY-MM` (monthly dedup) **Recommended actions**: * Notify the buyer to update their payment method * Optionally degrade the service (limit features rather than fully revoking) * **Do not** revoke access immediately — the PSP may retry the charge automatically **Deduplication**: At most one `past_due` event per subscription per calendar month. If still overdue next month, a new event fires. **Trigger**: Refund has been completed and funds returned. **Payload**: * `data.amount` — Refund amount (including tax) * `data.orderId` — Original order ID **Recommended actions**: * Revoke delivered digital goods (revoke licenses, disable downloads) * Update order status to "refunded" **Trigger**: Refund processing failed. **Recommended actions**: * Log the failure for manual review * Do not revoke goods (the refund was not completed) ### Subscription Lifecycle ```mermaid theme={"system"} stateDiagram-v2 [*] --> pending: Order created pending --> active: First payment succeeds
subscription.activated pending --> closed: Payment timeout active --> canceling: Cancellation requested
subscription.canceling active --> past_due: Renewal failed
subscription.past_due active --> active: Renewal succeeded
subscription.payment_succeeded active --> active: Product changed
subscription.updated canceling --> active: Cancellation withdrawn
subscription.uncanceled canceling --> canceled: Period ended
subscription.canceled past_due --> active: Overdue payment recovered
subscription.payment_succeeded past_due --> canceled: Final cancellation
subscription.canceled active --> expired: Term ended canceled --> [*] closed --> [*] expired --> [*] ``` | Terminal State | Meaning | Fires Webhook? | | -------------- | ---------------------------------------------------------- | :---------------------: | | `canceled` | Subscription terminated (buyer/merchant cancel or overdue) | `subscription.canceled` | | `closed` | Never activated — payment timed out | No | | `expired` | Fixed-term subscription naturally ended | No | *** ## Signature Verification **Always verify signatures in production.** Without verification, anyone can send forged requests to your endpoint. ### Algorithm ``` 1. Parse t (timestamp in ms) and v1 (Base64 signature) from X-Waffo-Signature header 2. Build signature input: `${t}.${rawRequestBody}` 3. Verify v1 using RSA-SHA256 with the Waffo public key 4. (Recommended) Check that t is within 5 minutes of current time to prevent replay attacks ``` ### Using the SDK (Recommended) The SDK embeds public keys, auto-detects the environment, and handles format normalization: ```typescript theme={"system"} import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts"; 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"); switch (event.eventType) { case WebhookEventType.OrderCompleted: // Deliver digital goods break; case WebhookEventType.SubscriptionActivated: // Provision subscription access break; case WebhookEventType.SubscriptionCanceled: // Revoke access break; } } catch { res.status(401).send("Invalid signature"); } }); ``` See the full [SDK Webhook documentation](/integrate/webhooks). ### Manual Verification If you're not using the TypeScript SDK, implement signature verification manually. ```javascript Node.js (Express) theme={"system"} const crypto = require('crypto'); // From Dashboard → Developers → Webhook Public Key (PEM format) const WAFFO_WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... -----END PUBLIC KEY-----`; function parseSignatureHeader(header) { const parts = {}; for (const pair of header.split(',')) { const [key, ...rest] = pair.split('='); parts[key.trim()] = rest.join('=').trim(); } return parts; } function verifyWebhookSignature(rawBody, signatureHeader, publicKey) { const { t, v1 } = parseSignatureHeader(signatureHeader); if (!t || !v1) return false; // Replay protection: 5-minute tolerance const tolerance = 5 * 60 * 1000; if (Math.abs(Date.now() - Number(t)) > tolerance) return false; // Verify RSA-SHA256 signature const signatureInput = `${t}.${rawBody}`; const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(signatureInput); return verifier.verify(publicKey, v1, 'base64'); } app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-waffo-signature']; const rawBody = req.body.toString('utf-8'); if (!sig || !verifyWebhookSignature(rawBody, sig, WAFFO_WEBHOOK_PUBLIC_KEY)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(rawBody); res.status(200).send('OK'); // Process asynchronously handleEvent(event).catch(console.error); } ); async function handleEvent(event) { switch (event.eventType) { case 'order.completed': await grantAccess(event.data.buyerEmail, event.data.productName); break; case 'subscription.activated': await createSubscription(event.data.buyerEmail, event.data.orderId); break; case 'subscription.payment_succeeded': await extendSubscription(event.data.orderId); break; case 'subscription.canceling': await markCanceling(event.data.orderId); break; case 'subscription.canceled': await revokeAccess(event.data.orderId); break; case 'subscription.past_due': await notifyPastDue(event.data.buyerEmail, event.data.orderId); break; case 'refund.succeeded': await revokeAccess(event.data.orderId); break; case 'refund.failed': await flagForReview(event.data.orderId); break; } } ``` ```python Python (Flask) theme={"system"} import json, time from base64 import b64decode from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from flask import Flask, request app = Flask(__name__) WAFFO_WEBHOOK_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... -----END PUBLIC KEY-----""" def verify_webhook_signature(raw_body, signature_header, public_key_pem): parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p) t, v1 = parts.get("t"), parts.get("v1") if not t or not v1: return False # Replay protection: 5-minute tolerance if abs(int(time.time() * 1000) - int(t)) > 5 * 60 * 1000: return False signature_input = f"{t}.{raw_body}".encode("utf-8") public_key = serialization.load_pem_public_key(public_key_pem.encode("utf-8")) try: public_key.verify(b64decode(v1), signature_input, padding.PKCS1v15(), hashes.SHA256()) return True except Exception: return False @app.route("/webhooks", methods=["POST"]) def handle_webhook(): sig = request.headers.get("X-Waffo-Signature", "") raw_body = request.get_data(as_text=True) if not verify_webhook_signature(raw_body, sig, WAFFO_WEBHOOK_PUBLIC_KEY): return "Invalid signature", 401 event = json.loads(raw_body) # Process event... return "OK", 200 ``` ```go Go (net/http) theme={"system"} package main import ( "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "io" "math" "net/http" "strconv" "strings" "time" ) const waffoPublicKeyPEM = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... -----END PUBLIC KEY-----` func verifyWebhookSignature(rawBody []byte, signatureHeader string) bool { var t, v1 string for _, pair := range strings.Split(signatureHeader, ",") { parts := strings.SplitN(pair, "=", 2) if len(parts) != 2 { continue } switch strings.TrimSpace(parts[0]) { case "t": t = strings.TrimSpace(parts[1]) case "v1": v1 = strings.TrimSpace(parts[1]) } } if t == "" || v1 == "" { return false } ts, err := strconv.ParseInt(t, 10, 64) if err != nil || math.Abs(float64(time.Now().UnixMilli()-ts)) > 5*60*1000 { return false } block, _ := pem.Decode([]byte(waffoPublicKeyPEM)) if block == nil { return false } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return false } rsaPub, ok := pub.(*rsa.PublicKey) if !ok { return false } hash := sha256.Sum256([]byte(fmt.Sprintf("%s.%s", t, string(rawBody)))) sig, err := base64.StdEncoding.DecodeString(v1) if err != nil { return false } return rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, hash[:], sig) == nil } func webhookHandler(w http.ResponseWriter, r *http.Request) { rawBody, _ := io.ReadAll(r.Body) if !verifyWebhookSignature(rawBody, r.Header.Get("X-Waffo-Signature")) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func main() { http.HandleFunc("/webhooks", webhookHandler) http.ListenAndServe(":8080", nil) } ``` **You must use the raw request body for signature verification.** If your framework parses JSON automatically, the signature check will fail. Ensure you capture the unmodified raw string before verification. *** ## Response Requirements * Return **2xx** status code (recommended: `200`) * Respond within **10 seconds** * Response body does not matter ```javascript theme={"system"} // Recommended: respond immediately, process async app.post('/webhooks', (req, res) => { res.status(200).send('OK'); processEventAsync(req.body); }); ``` Non-2xx responses or timeouts trigger retries. *** ## Retry Policy Failed deliveries are retried automatically with exponential backoff: | Item | Details | | ------------- | ------------------------------------------------------- | | Retries | Up to 3 (4 total attempts including the first) | | Strategy | Exponential backoff | | Timeout | Non-2xx or no response within the timeout window | | Final failure | Delivery marked as `failed` after all retries exhausted | ### Delivery Status | Status | Description | | --------- | ------------------------------------------------- | | `pending` | Created, awaiting delivery or retrying | | `success` | Delivered successfully (your server returned 2xx) | | `failed` | All retries exhausted | View delivery history in the Dashboard webhook logs, including status, HTTP response code, and response body (truncated to 1000 characters). *** ## Handling Duplicates Network issues may cause the same event to be delivered multiple times. **Ensure your event handling is idempotent.** Use the `eventType` + `eventId` combination (which has a unique constraint in the system) for deduplication: ```javascript theme={"system"} async function handleEvent(event) { const exists = await db.query( 'SELECT 1 FROM processed_webhooks WHERE event_type = $1 AND event_id = $2', [event.eventType, event.eventId] ); if (exists.rows.length > 0) return; // Already processed await processEventLogic(event); await db.query( 'INSERT INTO processed_webhooks (event_type, event_id, processed_at) VALUES ($1, $2, NOW())', [event.eventType, event.eventId] ); } ``` The same business event (identical `eventType` + `eventId`) only creates one delivery record — it won't be duplicated. However, a single delivery may reach your endpoint multiple times due to retries. *** ## Best Practices Always verify `X-Waffo-Signature`. Without verification, anyone can send forged requests to your endpoint. Production webhook URLs must use HTTPS to protect data in transit. Return `200` immediately and process business logic in the background. Slow responses cause unnecessary retries. Use the `eventType` + `eventId` combination to deduplicate. Ensure the same delivery processed multiple times has no side effects. Verify that the `t` timestamp is within 5 minutes of the current time to prevent replay attacks. Test and Production use different key pairs. Match the public key to the `mode` field in the payload. Store received payloads for debugging. The Dashboard also provides delivery log queries. Add a branch for every event you subscribe to, even if you don't need it yet. Return `200` for unhandled events — returning an error triggers unnecessary retries. *** ## Testing ### Send Test Events (Recommended) Use the Dashboard "Send Test Event" button to send test events without triggering real transactions. Test events use fixed sample data (amount 0, taxAmount 0, product "\[TEST] Webhook Verification") and are always signed with the Test key. All 10 event types are supported — test each one to verify your handler. ### Use Test Mode 1. Configure the Test environment Webhook URL and events in the Dashboard 2. Perform real operations in Test mode (create orders, process payments) 3. Events are sent to your Test Webhook URL with Test signing keys ### Local Development Use a tunnel to expose your local server: ```bash theme={"system"} ngrok http 8080 # Use the generated URL as your Test Webhook URL # e.g., https://abc123.ngrok.io/webhooks ``` *** ## Delivery Logs View webhook delivery history in the Dashboard: * **Status**: pending / success / failed * **HTTP status code**: Your server's response code * **Response body**: Your server's response (truncated to 1000 chars) * **Timestamp**: Last delivery attempt *** ## FAQ ### Not receiving webhooks 1. Confirm the Webhook URL is configured in the Dashboard and publicly accessible 2. Confirm you've subscribed to the correct event types 3. Confirm you're using the correct environment (Test / Production) 4. Check that your firewall allows requests from Waffo 5. Try the Dashboard "Send Test Event" to isolate the issue ### Signature verification fails 1. Confirm you're using the correct environment's public key (Test vs Production) 2. Confirm you're using the **raw request body** — not a parsed JSON object 3. Check if any middleware or proxy modified the request body 4. Confirm the signature input format is `${t}.${rawBody}` (timestamp + dot + raw body) 5. If using TypeScript, switch to the [`@waffo/pancake-ts`](https://www.npmjs.com/package/@waffo/pancake-ts) SDK — it handles key selection and format normalization automatically ### Receiving duplicate events This is normal retry behavior. If your endpoint returned non-2xx or timed out, the system retries. Ensure your handler is idempotent — use the `eventType` + `eventId` combination for deduplication. ### Difference between `subscription.canceling` and `subscription.canceled` * **`canceling`**: Cancellation requested, but the current paid period hasn't ended. The subscription is still active and the buyer can withdraw the cancellation (triggers `uncanceled`). **Do not revoke access.** * **`canceled`**: Subscription is terminated. This is irreversible — revoke access or downgrade permissions. ### What does `data.amount` mean for different events? All events: `data.amount` is the **transaction amount for that specific event** (including tax): * `order.completed` / `subscription.activated` — Payment amount * `subscription.payment_succeeded` — Renewal amount for this period * `subscription.past_due` — Amount due for this period * `refund.succeeded` / `refund.failed` — Refund amount * `subscription.canceling` / `subscription.canceled` / `subscription.uncanceled` — Subscription per-period amount # Checkout Flow Source: https://docs.waffo.ai/checkout/checkout-flow How your customers complete purchases ## Checkout Overview Waffo Pancake uses a **two-step checkout flow** optimized for conversion. This design collects customer details first, then payment information, reducing abandonment and supporting accurate tax calculation. ## Two Ways to Check Out Waffo Pancake supports two checkout methods — choose the one that fits your use case: **Permanent link** — never expires, stays valid even when you update the product. * Best for: sharing on websites, social media, email * No code required **Dynamic link** — created via API with custom parameters. * Best for: custom checkout flows, dynamic pricing, metadata * 45-minute default TTL (configurable up to 7 days) ### Product Purchase Links Every product gets a permanent purchase link: ``` https://checkout.waffo.ai/{store-slug}/{product-slug} ``` Test mode: ``` https://checkout.waffo.ai/{store-slug}/{product-slug}/test ``` A checkout session is created automatically when the customer opens the link. Just click "Copy Link" in the Dashboard. ### Checkout Sessions (API) For advanced use cases, create a checkout session via the API: ```bash theme={"system"} POST /v1/actions/checkout/create-session ``` ### Authenticated vs Anonymous Checkout When creating sessions via the API, you can choose between two modes: * **Authenticated**: Provide `buyerIdentity` to bind orders to a stable identifier. Pre-fills the checkout form and enables post-purchase self-service. * **Anonymous**: No identity required. Buyer fills in details manually. Suitable for shared links and template stores. Authenticated checkout is strongly recommended when you know the buyer. See [SDK Checkout Modes](/integrate/sdks#checkout-modes) for details. The resulting checkout URL follows this structure: ``` https://checkout.waffo.ai/{store-slug}/checkout/{sessionId} ``` | Component | Description | | ------------ | ------------------------------------------------------------------------------ | | `store-slug` | Your store's unique URL slug | | `sessionId` | Checkout session identifier (default 45-minute TTL, configurable up to 7 days) | Checkout sessions are generated server-side and lock the product version and price at creation time. ## Two-Step Checkout Flow Collect email, country, and billing info for tax calculation and order processing. Enter card details or use Apple Pay / Google Pay to complete payment. Checkout page — Consumer details *** ## Step 1: Consumer Details The first step collects information needed for tax calculation and order processing. ### Required Fields | Field | Purpose | | ------------- | ------------------------------------------------- | | Email address | Receipt, account creation, Customer Portal access | | Country | Tax jurisdiction determination | ### Conditional Fields (Based on Country) | Field | When Required | | --------------- | ------------------------------------------ | | ZIP/Postal Code | US, CA, UK, and most countries | | State/Province | US, CA, AU, IN | | City | EU countries (for VAT) | | Street Address | EU countries (full address for compliance) | ### Business Purchase Option Customers can toggle "I'm purchasing as a business" to provide: * Business name * Full billing address * Tax ID (optional, for VAT exemption) EU tax compliance requires full address for accurate VAT calculation and invoicing. *** ## Step 2: Payment Checkout — Payment The second step collects payment information. ### Payment Methods * Card number * Expiry date (MM/YY) * CVC/CVV * Cardholder name * Apple Pay (iOS, Safari) * Google Pay (Android, Chrome) * One-click with biometric auth ### Order Summary Displayed alongside payment form: | Element | Description | | ------------- | ------------------------------- | | Product name | What customer is buying | | Product image | Visual confirmation | | Subtotal | Product price | | Tax | Calculated from Step 1 location | | Total | Final amount to charge | ### Terms Agreement Customer must agree to terms before completing purchase. Links to: * Terms of Service * Privacy Policy * Merchant of Record disclosure *** ## Processing After payment submission: 1. **Card validation** - Luhn check, expiry, CVC 2. **3D Secure** - Bank verification (if required) 3. **Fraud check** - Risk assessment 4. **Authorization** - Charge to card 5. **Order creation** - Record in system Customer sees: * Animated loading indicator * "Processing your payment" message * Real-time status updates *** ## Result Pages ### Success Page On successful payment: * Order confirmation number * "Receipt sent to your email" message * Next steps or access instructions * Customer Portal link ### Failed Page On payment failure: * Clear error explanation * Suggested action * Retry button * Support contact option *** ## Subscription Checkout For subscription products, additional information displays: ### Billing Preview | Element | Example | | ----------------- | --------------------- | | Billing frequency | "Billed monthly" | | Recurring amount | "\$29.00/month" | | Next billing date | Calculated from today | ### Trial Information (if enabled) | Element | Example | | ----------------- | --------------------- | | Trial duration | "7-day free trial" | | First charge date | Date after trial ends | | Cancel anytime | Reassurance message | *** ## Multi-Language Support Checkout automatically displays in customer's browser language: | Language | Code | | -------------------- | ---- | | English | en | | Chinese (Simplified) | zh | | Japanese | ja | | Korean | ko | | German | de | | French | fr | | Spanish | es | *** ## Test Checkout Use Test Mode to try the checkout flow without real charges. ### Test Card Numbers | Card | Number | Result | | ----------------- | --------------------- | -------- | | Visa Credit | `4576 7500 0000 0110` | Success | | Mastercard Credit | `2226 9000 0000 0110` | Success | | Visa Debit | `4001 7000 0000 0110` | Success | | Mastercard Debit | `2226 9300 0000 0110` | Success | | Visa Credit | `4576 7500 0000 0220` | Declined | | Mastercard Credit | `2226 9000 0000 0220` | Declined | Any future expiry date and any 3-digit CVC will work. Test mode is indicated by a banner: "Test Mode - No real charges will be made" *** ## Error Handling ### Common Payment Errors | Error | Cause | Customer Action | | ------------- | -------------------------------- | ------------------ | | Card declined | Insufficient funds or bank block | Try different card | | Invalid card | Incorrect card number | Check and re-enter | | Expired card | Card past expiration | Use valid card | | CVC mismatch | Wrong security code | Re-enter CVC | | 3DS failed | Bank verification failed | Contact bank | ### Error Display Error messages are: * Clear and actionable * Non-technical language * Include helpful next steps * Do not expose sensitive data *** ## Checkout Security ### Product Purchase Links * Link is permanent and public — no sensitive data is embedded * A fresh checkout session is created server-side each time a customer opens the link * Product version and pricing are resolved at the moment of access ### Checkout Sessions (API) * Sessions lock the product version and price snapshot at creation time * Sessions include a cryptographic signature and cannot be tampered with * Sessions expire after 45 minutes by default (configurable up to 7 days via `expiresInSeconds`) Checkout sessions expire after 45 minutes by default. Use `expiresInSeconds` to extend up to 7 days. Customers must start a new session after expiration. *** ## Merchant of Record Notice At checkout footer, customers see: > "This order is processed by our online reseller & Merchant of Record, Waffo Pancake, who also handles order-related inquiries and returns." This disclosure is required for MoR compliance and helps customers understand the payment relationship. *** ## Next Steps Supported cards and wallets Brand your checkout experience # Checkout Customization Source: https://docs.waffo.ai/checkout/customization Brand your checkout experience with custom colors, logo, and style import ## Customization Overview Make your checkout page match your brand. Customize colors manually, or import styles directly from your website URL. ## Accessing Checkout Settings 1. Go to Dashboard → Settings 2. Select the "Checkout" tab 3. Customize and preview changes in real-time 4. Save to apply to both light and dark themes *** ## Import from Website URL The fastest way to brand your checkout — paste your website URL and Waffo Pancake automatically extracts your brand colors, border radius, and favicon. In the Checkout Settings panel, find the "Import from Website" section and paste your website URL (e.g., `https://yoursite.com`). Click the **Extract** button. The system analyzes your website's CSS and extracts brand colors. Extracted colors are applied to the current theme mode. Review the live preview and fine-tune any values if needed. ### How It Works Style extraction uses a 3-tier strategy for maximum accuracy: | Tier | Method | What It Extracts | | ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | | **1** | CSS Variables | `--primary`, `--brand`, `--accent` and similar CSS custom properties | | **2** | CSS Rule Parsing | Button backgrounds, heading colors, body styles from actual CSS rules | | **3** | AI Fallback | When tiers 1-2 yield insufficient results, an AI model analyzes the HTML/CSS context to identify brand colors | ### Extracted Properties | Property | Source | | ---------------- | ------------------------------------------------------- | | Primary Color | Brand/accent color from buttons and CTAs | | Background Color | Page background color | | Card Color | Derived from background color (slightly lighter/darker) | | Text Color | Main body text color | | Border Radius | Most common border-radius used on the site | | Logo | Site favicon (converted to base64) | If your store's website URL is already set in Store Settings, it will be pre-filled automatically. AI-assisted extraction is rate-limited to 10 requests per store per day. CSS-based extraction (tiers 1-2) has no limit. *** ## Logo Upload your brand logo to appear on the checkout page. | Setting | Specification | | ---------------- | ----------------------- | | Format | PNG, JPG, SVG | | Recommended Size | 200 × 50 px | | Max File Size | 2 MB | | Background | Transparent recommended | Use a horizontal logo for best results. When importing from a website URL, the site's favicon is automatically used as the logo. *** ## Theme Mode Each theme maintains its own set of color values. Switch between modes to customize each independently. White background, dark text. Best for most brands. Professional appearance. Dark background, light text. Modern aesthetic. Reduces eye strain. *** ## Color Settings Customize the checkout color scheme for the current theme mode: | Setting | Field Name | Default (Light) | Default (Dark) | | -------------------- | ------------------------- | --------------- | -------------- | | **Primary Color** | `checkoutColorPrimary` | #7CCB02 | #ADFF85 | | **Background Color** | `checkoutColorBackground` | #FFFFFF | #0A1A1F | | **Card Color** | `checkoutColorCard` | #F3F4F6 | #0A1A1F | | **Text Color** | `checkoutColorText` | #111827 | #FFFFFF | For each color setting, click the color swatch to open the color picker, or enter a hex code directly. Ensure sufficient contrast between text and background colors for accessibility. WCAG recommends a contrast ratio of at least 4.5:1. ### Border Radius Control the roundness of UI elements with the `checkoutBorderRadius` setting. | Value | Style | | ------ | -------------------- | | `0px` | Sharp corners | | `4px` | Subtle rounding | | `8px` | Standard (default) | | `12px` | Large rounding | | `16px` | Extra-large rounding | *** ## Live Preview The preview panel shows your checkout page in real-time as you make changes. * **Desktop view** — Full-width checkout layout * **Mobile view** — Phone-sized checkout layout * **Product selector** — Preview with different products from your store Changes update instantly in the preview without saving. *** ## API Reference Checkout settings are saved as part of the store's `checkoutSettings` object: ```json theme={"system"} { "checkoutSettings": { "light": { "checkoutLogo": "https://...", "checkoutColorPrimary": "#7CCB02", "checkoutColorBackground": "#FFFFFF", "checkoutColorCard": "#F3F4F6", "checkoutColorText": "#111827", "checkoutBorderRadius": "8px" }, "dark": { "checkoutLogo": null, "checkoutColorPrimary": "#ADFF85", "checkoutColorBackground": "#0A1A1F", "checkoutColorCard": "#0A1A1F", "checkoutColorText": "#FFFFFF", "checkoutBorderRadius": "8px" } } } ``` Update via the [Update Store](/api-reference/endpoints/stores) endpoint with `checkoutSettings` in the request body. *** ## Store Information | Option | Description | | ------------- | ---------------------------------------- | | Store Name | Shown in checkout header | | Support Email | For customer questions | | Terms Link | Link to your terms of service (required) | | Privacy Link | Link to your privacy policy (required) | *** ## Best Practices * Use the **Import from Website** feature to automatically match your site's colors * Use the same logo as your main site * Consistent color palette builds trust * Many customers use dark mode — customize both light and dark themes * Ensure your logo works on both light and dark backgrounds * Test color contrast in both modes * Use the mobile preview to check layout * Ensure logos scale properly on small screens * Verify touch targets are adequate * Maintain sufficient color contrast (4.5:1 minimum) * Readable font sizes * Test with different color vision profiles *** ## Reset to Default To reset the current theme mode to default: 1. Click "Reset" in the Checkout Settings 2. Default Waffo Pancake colors will be restored for the current mode (light or dark) Resetting cannot be undone. Your current customizations for that theme mode will be lost. # Analytics Source: https://docs.waffo.ai/dashboard/analytics Business insights and performance metrics ## Analytics Overview The Analytics page provides comprehensive insights into your business performance with interactive charts and key metrics. Analytics Dashboard *** ## Key Metrics ### Revenue Metrics | Metric | Description | | ----------------------- | -------------------------------------------- | | **Total Revenue** | Sum of all successful payments | | **MRR** | Monthly Recurring Revenue from subscriptions | | **ARR** | Annual Recurring Revenue (MRR × 12) | | **Average Order Value** | Average transaction amount | ### Customer Metrics | Metric | Description | | --------------------------- | --------------------------------- | | **Total Customers** | Unique paying customers | | **New Customers** | Customers acquired in period | | **Repeat Customers** | Customers with multiple purchases | | **Customer Lifetime Value** | Average revenue per customer | ### Subscription Metrics | Metric | Description | | ------------------------ | ------------------------------ | | **Active Subscriptions** | Currently active subscriptions | | **Churn Rate** | % of subscriptions canceled | | **Net MRR Growth** | MRR change over period | | **Trial Conversion** | % of trials converting to paid | *** ## Charts & Visualizations ### Revenue Chart Interactive line chart showing: * Daily/weekly/monthly revenue * Trend lines and comparisons * Period-over-period growth **Time Ranges:** * Last 7 days * Last 30 days * Last 90 days * Last 12 months * Custom range ### Transaction Volume Bar chart displaying: * Number of transactions per period * Successful vs failed breakdown * Average transaction size ### Customer Geography Visual breakdown of customers by location: * Country distribution map * Top markets by revenue * Regional growth trends See which countries generate the most revenue. Revenue breakdown by transaction currency. *** ## Date Range Selection ### Quick Filters | Filter | Period | | ------------ | ---------------- | | Today | Current day | | Last 7 Days | Previous week | | Last 30 Days | Previous month | | Last 90 Days | Previous quarter | | This Year | Year to date | ### Custom Range Select specific start and end dates for detailed analysis. ### Comparison Enable comparison mode to see: * Period-over-period changes * Percentage growth/decline * Trend indicators *** ## Metric Cards Each metric card displays: | Element | Description | | --------------- | ---------------------------------- | | **Value** | Current metric value | | **Change** | % change from previous period | | **Trend Arrow** | Up (green) or down (red) indicator | | **Sparkline** | Mini chart showing recent trend | Click on any metric card to see a detailed breakdown and drill-down analysis. *** ## Export & Reports ### Export Options | Format | Use Case | | -------- | ------------------------------------- | | **CSV** | Data analysis in spreadsheets | | **PDF** | Professional reports for stakeholders | | **JSON** | Programmatic access via API | ### Scheduled Reports Set up automated reports: * Daily summary emails * Weekly performance digest * Monthly business review Configure in **Settings → Notifications**. *** ## Filtering & Segmentation ### Filter by Product View analytics for specific products: * Individual product performance * Product comparison * Best sellers ranking ### Filter by Customer Segment Segment analytics by: * New vs returning customers * Subscription tier * Geographic region ### Filter by Payment Method Analyze by payment type: * Card payments * Apple Pay / Google Pay * Regional payment methods *** ## Understanding Your Data ### Test vs Live Mode Analytics data is separate for Test and Live modes. Ensure you're viewing the correct environment. ### Data Freshness | Data Type | Update Frequency | | ------------------ | ---------------- | | Transactions | Real-time | | Aggregated Metrics | Every 5 minutes | | Charts | Every 15 minutes | ### Timezone All analytics use UTC timezone. Timestamps are converted to your local timezone for display. *** ## Best Practices Focus on metrics that matter: * MRR for subscription health * Churn rate for retention * LTV for customer value Compare against: * Your historical performance * Industry averages * Growth targets Schedule time to review: * Daily: Transaction volume * Weekly: Revenue trends * Monthly: Full business review # Customers Source: https://docs.waffo.ai/dashboard/customers Manage your customer database and relationships ## Customer Management The Customers page provides a complete view of everyone who has interacted with your products. Customers List ## Customer List View all your customers in a sortable, filterable table: | Column | Description | | ------------- | -------------------------------------------- | | First Seen | When the customer first appeared | | Email | Customer email address | | Country | Customer location (with flag) | | Status | Current customer status | | Subscriptions | Active subscription count | | Payments | Total payment count | | MRR | Monthly Recurring Revenue from this customer | | Revenue | Total lifetime revenue | ## Customer Statuses Has made payments but no active subscriptions. Has one or more active subscriptions. Previously had subscriptions that are now canceled. No recent activity or purchases. ## Customer Details Click on a customer row to view their full profile: ### Overview Tab * Email and name * Location * Account creation date * Total lifetime value * Current MRR ### Subscriptions Tab * All subscriptions (active and past) * Subscription status * Billing interval * Next billing date * Actions (pause, cancel) ### Payments Tab * Full payment history * Payment status * Amounts and currencies * Refund status * Associated products ### Activity Tab * Login history * Subscription changes * Payment events * Support interactions ## Filtering Customers Use filters to segment your customer base: | Filter | Options | | ---------------- | ------------------------------------- | | Status | Active, Subscribed, Churned, Inactive | | Country | Any country | | Date Range | First seen date | | Has Subscription | Yes/No | ### Search Search customers by: * Email address * Customer name * Customer ID ## Customer Metrics ### Statistics Cards | Metric | Description | | --------------- | ------------------------------- | | Total Customers | All unique customers | | Active | Customers with recent activity | | Countries | Number of different countries | | Avg. LTV | Average customer lifetime value | ### Geography Distribution A breakdown of your customers by country: * Top 5 countries displayed * Customer count per country * Revenue per region ## Customer Actions Generate a link to let customers manage their own subscriptions. Process a one-time charge for an existing customer. Cancel a customer's active subscription immediately or at period end. Issue a full or partial refund for a past payment. Download customer information for GDPR compliance or analysis. ## Customer Portal Waffo Pancake provides a self-service portal where customers can: * View their active subscriptions * Update payment methods * Download invoices * Cancel subscriptions * View payment history To give a customer access: 1. Go to customer details 2. Click "Generate Portal Link" 3. Send the secure link to the customer Portal links are time-limited for security. Generate a new link if the customer needs extended access. ## Data Export Export your customer data in CSV format: 1. Apply any desired filters 2. Click "Export" 3. Choose fields to include 4. Download the CSV file Available export fields: * Email * Name * Country * Status * Created date * Total revenue * MRR * Subscription count # Home Source: https://docs.waffo.ai/dashboard/home Your business overview at a glance ## Home Page Overview The Dashboard Home page provides a comprehensive view of your business performance with key metrics, charts, and recent activity. Dashboard Home ## Statistics Cards At the top of the Home page, you'll find key performance indicators: | Metric | Description | | ------------------------ | --------------------------------------------------- | | **Total Revenue** | Sum of all successful payments | | **MRR** | Monthly Recurring Revenue from active subscriptions | | **Customers** | Total number of unique customers | | **Sales** | Number of successful one-time payments | | **Active Subscriptions** | Currently active subscription count | Statistics are calculated based on your current mode (Test or Live). Make sure you're viewing the correct mode for accurate data. ## Charts and Visualizations ### Transaction Volume Chart An interactive line chart showing: * Daily transaction counts * Revenue trends over time * Selectable date ranges (7 days, 30 days, 90 days) ### Customer Geography A visual breakdown of your customers by region: * Country distribution * Top markets * Geographic revenue concentration ## Recent Activity ### Recent Transactions A table showing your latest payments: | Column | Description | | -------- | ------------------------------------ | | Date | Transaction timestamp | | Customer | Customer email | | Amount | Payment amount with currency | | Status | Payment status (Paid, Pending, etc.) | | Product | Associated product name | ### Recent Customers Your newest customer signups with: * Customer email * Sign-up date * Country * First purchase status ## Quick Actions From the Home page, you can quickly: Add a new product to your catalog See all transactions Download reports ## Data Refresh Dashboard data updates automatically. You can also: * Pull to refresh on mobile * Click the refresh icon to force update * Data typically updates within seconds of new transactions ## Time Periods Filter your Home page data by time period: * **Today** - Current day only * **Last 7 Days** - Previous week * **Last 30 Days** - Previous month * **Last 90 Days** - Previous quarter * **Custom Range** - Select specific dates ## Currency Display * Main totals show in your default currency * Individual transactions show in their original currency * Exchange rates are applied for aggregations # Payments Source: https://docs.waffo.ai/dashboard/payments Track transactions and manage refunds ## Payments Overview The Payments page shows all transactions processed through your store, including one-time purchases and subscription charges. Payments List ## Payment List View all payments in a comprehensive table: | Column | Description | | -------------- | ----------------------------------- | | Date | Transaction timestamp | | Amount Paid | Gross payment amount | | Tax Amount | Tax collected | | Status | Payment status | | Payment Method | Card type and last 4 digits | | Description | Payment description or product name | | Customer | Customer email | | Product | Associated product | ## Payment Statuses Successfully processed and funds received. Payment initiated but not yet confirmed. Full amount has been refunded. Part of the payment has been refunded. Payment was rejected by the payment processor. Customer disputed the charge with their bank. ## Filtering Payments Filter your payment list by: | Filter | Options | | ---------- | -------------------------------------------------- | | Status | All, Paid, Pending, Refunded, Declined, Chargeback | | Product | Any product in your catalog | | Date Range | Custom start and end dates | | Customer | Search by email | | Amount | Min/Max amount range | ### Search Search payments by: * Payment ID * Customer email * Product name * Last 4 digits of card ## Payment Details Click on any payment to view full details: ### Transaction Info * Payment ID * Amount and currency * Tax amount * Net amount (after fees) * Timestamp * Test/Live mode indicator ### Payment Method * Card brand (Visa, Mastercard) * Last 4 digits * Expiration date * Country of issue ### Customer Info * Email * Name (if provided) * IP address * Country ### Associated Records * Product purchased * Subscription ID (if applicable) * Invoice link ## Processing Refunds Locate the payment in the list or use search. Open the payment details and click the Refund button. * **Full Refund** - Return the entire amount * **Partial Refund** - Enter a custom amount Document why the refund is being processed. Review and confirm the refund. Refunds typically take 5-10 business days to appear on the customer's statement. Refunds cannot be reversed once processed. ### Refund Policies | Scenario | Recommendation | | -------------------- | ---------------------------------- | | Duplicate charge | Full refund | | Partial service used | Partial refund | | Service issue | Full or partial based on situation | | Fraud | Full refund + review account | ## Payment Statistics At the top of the Payments page: | Metric | Description | | ---------------- | ---------------------------- | | Total Revenue | Sum of all paid transactions | | Net Revenue | Total revenue minus refunds | | Total Refunded | Sum of all refunds issued | | Successful | Count of successful payments | | Avg. Order Value | Average payment amount | ## Payment Methods Waffo Pancake supports: Visa, Mastercard Available on iOS and Safari Available on Android and Chrome ## Chargebacks When a customer disputes a charge: 1. You'll receive a notification 2. Payment status changes to "Chargeback" 3. Funds are held pending resolution 4. You can submit evidence to fight the dispute Chargebacks carry fees and can impact your merchant reputation. Prevent them with clear product descriptions, good customer service, and recognizable billing descriptors. ## Exporting Payments Export payment data for accounting or analysis: 1. Apply desired filters 2. Click "Export" 3. Select format (CSV or PDF) 4. Download the file Export includes: * All payment details * Customer information * Tax amounts * Refund status * Timestamps # Products Source: https://docs.waffo.ai/dashboard/products Create and manage your product catalog ## Product Management The Products page is where you manage your entire product catalog. Create one-time purchase items or subscription products. Products List ## Product List Your products are displayed in a table with: | Column | Description | | ----------- | ---------------------------------------------------------- | | Name | Product name and image thumbnail | | Price | Price with currency and billing interval | | Status | Active or Inactive | | Subscribers | Number of active subscriptions (for subscription products) | | Created | Creation date | | Actions | Edit, copy link, toggle status | ## Creating a Product Create Product Find the button in the top right of the Products page. **Required Fields:** * Product Name * Price * Currency * Pricing Type (One-Time or Subscription) * Product Description (supports Markdown) * Product Image * Tax settings For subscription products only: * Billing Interval * Trial Period (optional) Click "Create" to publish your product. ## Product Fields ### Basic Information | Field | Required | Description | | ----------- | -------- | -------------------------------------- | | Name | Yes | Display name (shown to customers) | | Description | No | Product details in Markdown format | | Image | No | Product image (recommended: 400x400px) | ### Pricing | Field | Options | Description | | ------------ | ------------------------------------------ | --------------------------------------------- | | Price | Number | The product price (in smallest currency unit) | | Currency | USD, EUR, GBP, CNY, JPY, HKD, etc. | Transaction currency | | Pricing Type | One-Time, Subscription | Payment model | | Tax Category | SaaS, Digital Goods, Software, eBook, etc. | Tax calculation rules | | Tax Behavior | Inclusive, Exclusive | Price includes or excludes tax | ### Price Limits Each currency has a minimum and maximum price you can set on a product. Values outside this range are rejected when you save the product. | Currency | Minimum | Maximum | | -------- | -------: | ------------: | | USD | \$1.00 | \$7,500.00 | | EUR | €1.00 | €7,400.00 | | GBP | £1.00 | £6,400.00 | | JPY | ¥100 | ¥1,250,000 | | HKD | HK\$8.00 | HK\$61,000.00 | CNY is available for display in your dashboard analytics, but cannot currently be used as a product pricing currency. ### Subscription Settings Only applicable for subscription products: | Field | Options | Description | | ------------- | ---------------------------------- | --------------------------- | | Interval | Weekly, Monthly, Quarterly, Yearly | Billing frequency | | Trial Enabled | Yes/No | Offer free trial | | Trial Days | Number | Trial duration (default: 7) | ### Tax Configuration | Field | Options | Description | | ------------ | -------------------- | ------------------------------ | | Tax Category | SaaS, Digital Goods | Tax calculation rules | | Tax Behavior | Inclusive, Exclusive | Price includes or excludes tax | ## Product Status * Product is live and purchasable * Payment links work * Shown in checkout * Product is hidden * Payment links return error * Existing subscriptions continue To toggle status, use the switch in the product row or edit the product. ## Editing Products 1. Click on a product row or the edit icon 2. Modify any fields 3. Click "Save Changes" Changing the price of a subscription product only affects new subscribers. Existing subscribers keep their original price. ## Product Purchase Links Every product gets a permanent purchase link that never expires: ``` https://checkout.waffo.ai/{store-slug}/{product-slug} ``` Test mode: ``` https://checkout.waffo.ai/{store-slug}/{product-slug}/test ``` **Get your link:** * Click the "Copy Link" button on any product * For advanced use cases, create a Checkout Session via API with custom parameters (dynamic link, 7-day TTL) **Test vs Live:** * Toggle Test Mode in Dashboard header to preview with test links * Test links use the `/test` suffix ## Best Practices * Use clear, descriptive names * Include the billing period for subscriptions (e.g., "Pro Plan - Monthly") * Avoid special characters * List key features * Use bullet points for readability * Include what customers get * Mention support or guarantees * Use high-quality images (400x400px minimum) * PNG or JPG format * Keep file size under 2MB * Consistent style across products * Research competitor pricing * Consider regional purchasing power * Offer annual discounts (typically 15-20% off) * Round prices for simplicity ## Bulk Actions Select multiple products to: * Activate/Deactivate in bulk * Export product data * Delete (with confirmation) # Revenue Source: https://docs.waffo.ai/dashboard/revenue Per-store income from successful payments The **Revenue** page in the sidebar shows the income flow for the **current store**. Each row is a successful payment that contributed revenue. Store Revenue page ## Income Table | Column | Description | | ----------- | ---------------------------------------------------------- | | Date | When the payment was completed | | Description | Payment details (product name, subscription renewal, etc.) | | Gross | Total amount the buyer paid | | Fees | Processing fees deducted from the gross amount | | Net | Net amount after fees | ## How it relates to Merchant Finance Each store's settled revenue flows automatically into your **merchant balance** — there is no per-store withdrawal. To request a payout or see your pooled balance, open [Merchant Finance](/merchant/finance) from the user menu (top-right avatar). # Subscriptions Source: https://docs.waffo.ai/dashboard/subscriptions Manage recurring billing and subscriber relationships ## Subscription Management The Subscriptions page shows all recurring billing arrangements with your customers. Subscriptions List ## Subscription List View all subscriptions in a detailed table: | Column | Description | | ------------ | -------------------------------- | | Status | Current subscription status | | Product | Product name and image | | Customer | Subscriber email | | Amount | Subscription price with currency | | Interval | Billing frequency | | Next Billing | Next charge date | | Created | When subscription started | ## Subscription Statuses Subscription is live and billing normally. Customer is in free trial period. Payment failed, awaiting retry. Cancellation requested, active until period end. Subscription has expired. Subscription has been terminated. ## Billing Intervals | Interval | Billing Frequency | MRR Calculation | | --------- | ----------------- | --------------- | | Weekly | Every 7 days | Amount × 4.33 | | Monthly | Every month | Amount × 1 | | Quarterly | Every 3 months | Amount ÷ 3 | | Yearly | Every 12 months | Amount ÷ 12 | ## Filtering Subscriptions | Filter | Options | | -------- | -------------------------------------------------------- | | Status | Active, Trialing, Past Due, Canceling, Expired, Canceled | | Product | Any subscription product | | Interval | Weekly, Monthly, Quarterly, Yearly | | Customer | Search by email | ## Subscription Details Click on a subscription to view: ### Overview * Subscription ID * Status and status history * Product details * Pricing and currency * Billing interval ### Billing Info * Current period start/end * Next billing date * Payment method on file * Billing history ### Customer * Customer email * Account details * Other subscriptions from same customer ### Payment History * All charges for this subscription * Successful and failed attempts * Refunds ## Managing Subscriptions ### Cancel Subscription Click on the subscription you want to cancel. Find the button in the actions menu. Cancellation takes effect at the end of the current billing period. The subscription enters "Canceling" status until the period ends, then becomes "Canceled". The customer retains access until the end of their current paid billing period. ### Resume Subscription For subscriptions in "Canceling" status, a reactivation endpoint exists but currently returns 501 (not yet implemented). ## Subscription Metrics ### Statistics Cards | Metric | Description | | -------- | ------------------------------- | | MRR | Monthly Recurring Revenue | | Active | Count of active subscriptions | | Trialing | Count of subscriptions in trial | | Canceled | Count of canceled (this period) | | ARPU | Average Revenue Per User | ### MRR Calculation MRR is calculated from all active subscriptions: ``` MRR = Σ (Subscription Amount × Interval Factor) Where Interval Factor: - Weekly: 4.33 - Monthly: 1 - Quarterly: 0.33 - Yearly: 0.083 ``` ## Failed Payments When a subscription payment fails: 1. Status changes to "Past Due" 2. Automatic retry attempts (3 attempts over 7 days) 3. Customer notified via email 4. If all retries fail, subscription may cancel ### Dunning Emails Automatic emails sent to customers: * First failed payment notification * Retry attempt reminders * Final warning before cancellation * Cancellation confirmation ## Trial Periods For products with trials enabled: | Field | Description | | --------------- | ------------------------ | | Trial Status | Active trial indicator | | Trial Days | Total trial length | | Days Remaining | Time left in trial | | Conversion Date | When first charge occurs | Track trial conversions: * Trial → Active (converted) * Trial → Canceled (churned before conversion) ### Platform Trial Protection Waffo Pancake automatically tracks consumer trial history at the platform level. The actual trial days granted to a consumer may be less than the configured value if the consumer has used trials before. This prevents trial abuse without requiring any action from the merchant. For Product Groups with **shared trial** enabled, trial usage is shared across all products in the group. ## Subscription Actions Upgrade or downgrade the subscription to a different product. Note: this endpoint currently returns 501 (not yet implemented). Send customer a link to update their card on file. # Feature Overview Source: https://docs.waffo.ai/feature-list Everything Waffo Pancake offers at a glance ## Platform Features Waffo Pancake is an all-in-one Merchant of Record payment platform. Here's everything it offers, organized by module. *** ## Products & Pricing Create one-time and subscription products. Multi-currency pricing, tax categories, immutable versioning. Manage product lifecycle via Dashboard or API. Full subscription lifecycle: weekly/monthly/quarterly/yearly billing, free trials, dunning, cancellation, and reactivation. Product groups for plan switching. *** ## Payments & Checkout Two-step optimized checkout flow. Theme customization (brand colors, logo, dark mode), 7 languages, Apple Pay and Google Pay. Real-time order status and payment tracking. One-time and subscription orders with automatic tax calculation and collection. Ticket-based refund system. Partial and full refunds, 14-day refund window, built-in approval workflow. View balance, payout history, and transaction details. Configure bank accounts for payouts. Multiple payout schedules. *** ## Customers & Operations View customer list, purchase history, and subscription status. Automatic email-based customer association. Built-in self-service portal. Customers log in via Magic Link to view orders, manage subscriptions, request refunds, and update payment methods. 12 built-in email templates: order confirmation, subscription confirmation, renewal reminders, cancellation notices, and more. Merchant and customer notifications. Real-time revenue dashboard: MRR, Churn Rate, LTV, ARPU, and more. Trend analysis, distribution analysis, and customer insights — no SQL needed. *** ## Developer Tools TypeScript SDK, REST API + GraphQL, Webhook notifications, API Key management. AI-friendly SDK Skill integration. Complete test environment isolated from production. Use test card numbers to simulate payments and test the full flow. One-click test/live toggle. *** ## Compliance & Review Waffo Pancake acts as your Merchant of Record, handling global tax calculation, collection, and reporting. Accept payments compliantly without registering a company. Submit store details for qualification review. Production mode automatically enabled after approval. *** ## Get Started Complete flow from sign-up to your first payment. One-click payment integration with AI coding assistants. # Analytics Source: https://docs.waffo.ai/features/analytics Monitor store performance with charts, metrics, and flexible date ranges ## Analytics Overview The Analytics page lets you monitor your store's performance over time. It provides revenue charts, metric cards, and configurable date ranges so you can understand how your business is trending. *** ## Date Range & Granularity ### Date Range Picker Select a preset or custom date range to scope all charts and metrics on the page. | Preset | Period | | ---------- | ----------------------------------- | | **7d** | Last 7 days | | **14d** | Last 14 days | | **30d** | Last 30 days | | **90d** | Last 90 days | | **Custom** | Choose specific start and end dates | ### Granularity Control how data points are grouped in charts. | Granularity | Description | | ----------- | ------------------------ | | **Daily** | One data point per day | | **Weekly** | One data point per week | | **Monthly** | One data point per month | Your selected date range and granularity are saved automatically and persist across sessions, so the dashboard remembers your preferences. *** ## Revenue Charts The main visualization is a revenue chart that plots performance over the selected date range at the chosen granularity. Use the date range and granularity controls to adjust the view. *** ## Metric Cards The analytics page displays summary metric cards for key performance indicators such as revenue and customer counts. Each card shows the metric value for the selected period. *** ## Product Filtering You can filter analytics data by a specific product to see how individual items in your catalog are performing. This narrows charts and metrics to transactions associated with the selected product. *** ## Test vs Live Environment Analytics data is separate for Test and Live modes. Make sure you are viewing the correct environment using the mode toggle in the dashboard header. Switching between test and production mode updates all charts and metrics to reflect data from that environment only. Test mode data is never mixed with live data. *** ## Locale-Aware Formatting Numbers, currencies, and dates are formatted according to your locale setting. The dashboard supports English, Chinese, and Japanese formatting conventions. # Checkout Source: https://docs.waffo.ai/features/checkout A two-step checkout flow optimized for conversion ## Checkout Overview Waffo Pancake uses a **two-step checkout flow** optimized for conversion. This design collects customer details first, then payment information, reducing abandonment and supporting accurate tax calculation. *** ## Checkout URLs ### Product Purchase Links (Recommended) Every product gets a permanent purchase link that never expires: ``` https://checkout.waffo.ai/{store-slug}/{product-slug} ``` Test mode: ``` https://checkout.waffo.ai/{store-slug}/{product-slug}/test ``` Just click "Copy Link" in the Dashboard. The link stays the same even when you update the product. A checkout session is created automatically when the customer opens the link. ### Checkout Sessions (API) For advanced use cases (dynamic pricing, custom metadata, programmatic checkout), create a session via the API: ``` https://checkout.waffo.ai/{store-slug}/checkout/{sessionId} ``` | Component | Description | | ------------ | --------------------------------------- | | `store-slug` | Your store's unique URL slug | | `sessionId` | Checkout session identifier (7-day TTL) | Checkout sessions are generated server-side and lock the product version, pricing, and currency at creation time. *** ## Two-Step Flow Collect email, country, and billing info for tax calculation and order processing. Enter card details or use Apple Pay / Google Pay to complete payment. Checkout page — Consumer details ### Step 1: Consumer Details Collects information needed for tax calculation and order processing. **Required Fields:** | Field | Purpose | | ------------- | ------------------------------------------------- | | Email address | Receipt, account creation, Customer Portal access | | Country | Tax jurisdiction determination | **Conditional Fields (Based on Country):** | Field | When Shown | | --------------- | ---------------------------------------------- | | ZIP/Postal Code | Most countries (US, CA, UK, etc.) | | State/Province | US, CA, AU, IN, and other applicable countries | **Business Purchase Option:** Customers can toggle "I'm purchasing as a business" to provide: * Business name * Tax ID (for VAT/tax exemption) ### Step 2: Payment Checkout — Payment * Card number * Expiry date (MM/YY) * CVC/CVV * Cardholder name * Apple Pay (iOS, Safari) * Google Pay (Android, Chrome) * One-click with biometric auth **Order Summary** is displayed alongside the payment form with product name, image, subtotal, tax, and total. *** ## Subscription Checkout For subscription products, additional billing information displays: | Element | Example | | ----------------- | ------------------------------------------------------ | | Billing frequency | "Billed monthly" | | Recurring amount | "\$29.00/month" | | Next billing date | Calculated from today | | Trial duration | "7-day free trial" (if the product has trials enabled) | *** ## Customization Make your checkout page match your brand through the Checkout Settings in your dashboard. ### Logo Upload a custom logo displayed on the checkout page. Supported formats include PNG, JPG, and SVG. ### Theme Mode White background, dark text. Best for most brands. Dark background, light text. Modern aesthetic. Switch between Light and Dark mode presets. Each mode maintains its own set of color values. ### Color Settings | Setting | Field Name | Default (Light) | Default (Dark) | | -------------------- | ------------------------- | --------------- | -------------- | | **Primary Color** | `checkoutColorPrimary` | #7CCB02 | #ADFF85 | | **Background Color** | `checkoutColorBackground` | #FFFFFF | #0A1A1F | | **Card Color** | `checkoutColorCard` | #F3F4F6 | #0A1A1F | | **Text Color** | `checkoutColorText` | #111827 | #FFFFFF | ### Border Radius Control the roundness of UI elements with the `checkoutBorderRadius` setting. Default is `8px`. Options range from 0px (sharp corners) to 16px (extra-large rounding). Ensure sufficient contrast between text and background colors for accessibility. WCAG recommends a contrast ratio of at least 4.5:1. ### Store Information | Option | Description | | ------------- | ---------------------------------------- | | Store Name | Shown in header | | Support Email | For customer questions | | Terms Link | Link to your terms of service (required) | | Privacy Link | Link to your privacy policy (required) | *** ## Multi-Language Support Checkout automatically displays in the customer's browser language: | Language | Code | | -------------------- | ---- | | English | en | | Chinese (Simplified) | zh | | Japanese | ja | *** ## Processing & Results After payment submission: 1. **Card validation** -- Luhn check, expiry, CVC verification 2. **3D Secure** -- Bank verification (if required by the issuing bank) 3. **Authorization** -- Charge to card 4. **Order creation** -- Record in system **Success Page:** Order confirmation number, receipt email notification, next steps, Customer Portal link. **Failed Page:** Clear error explanation, suggested action, retry button, support contact option. *** ## Error Handling | Error | Cause | Customer Action | | ------------- | -------------------------------- | ------------------ | | Card declined | Insufficient funds or bank block | Try different card | | Invalid card | Incorrect card number | Check and re-enter | | Expired card | Card past expiration | Use valid card | | CVC mismatch | Wrong security code | Re-enter CVC | | 3DS failed | Bank verification failed | Contact bank | *** ## Test Checkout Use [Test Mode](/features/test-mode) to try the checkout flow without real charges. | Card | Number | Result | | ----------------- | --------------------- | -------- | | Visa Credit | `4576 7500 0000 0110` | Success | | Mastercard Credit | `2226 9000 0000 0110` | Success | | Visa Credit | `4576 7500 0000 0220` | Declined | Test mode is indicated by a banner: "Test Mode - No real charges will be made" *** ## Merchant of Record Notice At checkout footer, customers see: > "This order is processed by our online reseller & Merchant of Record, Waffo Pancake, who also handles order-related inquiries and returns." This disclosure is required for MoR compliance and helps customers understand the payment relationship. # Customer Management Source: https://docs.waffo.ai/features/customer-management View and track your customers from the merchant dashboard ## Customer Management The Customers page in the merchant dashboard gives you a consolidated view of every consumer who has purchased from your store. Track revenue, monitor subscriptions, and drill into individual customer details. *** ## Customer Data Each customer record contains the following fields, sourced from the backend `Customer` type: | Field | Description | | ------------------- | ----------------------------------------------------------- | | `id` | Unique customer identifier (UUID) | | `email` | Customer email address | | `name` | Customer name | | `country` | Customer country | | `totalRevenue` | Lifetime revenue from this customer (display format string) | | `mrr` | Monthly Recurring Revenue attributed to this customer | | `subscriptionCount` | Number of subscriptions (active and past) | | `paymentCount` | Total number of payments | | `createdAt` | When the customer record was created | | `updatedAt` | When the customer record was last updated | | `storeId` | The store this customer belongs to | The backend `Customer` type does not have a `status` field. Any status labels shown in the dashboard UI (such as "Active" or "Subscribed") are derived from the customer's subscription and payment activity, not stored as a property on the customer record itself. *** ## Customer List The dashboard displays customers in a sortable, filterable table with these columns: | Column | Description | | ------------- | -------------------------------------------- | | Email | Customer email address | | Country | Customer country (displayed with flag emoji) | | Revenue | Total lifetime revenue | | MRR | Monthly Recurring Revenue | | Subscriptions | Subscription count | | Payments | Payment count | | Created | When the customer first appeared | ### Search and Filtering The customer list supports: * **Text search** -- Search by customer email or name * **Status filtering** -- Filter by derived status categories based on subscription and payment activity * **Date range filtering** -- Filter by customer creation date *** ## Customer Details Click on a customer row to view their full profile. ### Overview * Email and name * Country (with flag emoji) * Account creation date * Total lifetime revenue * Current MRR * Subscription count and payment count ### Subscriptions View all subscriptions associated with this customer: * Subscription status * Product and tier information * Billing period * Dates and amounts ### Payments View the full payment history for this customer: * Payment status * Amounts and currencies * Associated orders and products * Refund status where applicable *** ## Customer Portal Customers also have access to a self-service **Customer Portal** where they can manage their own subscriptions and view payment history. The portal is a separate feature -- see the [Customer Portal](/features/customer-portal) page for details. Key capabilities available to customers through the portal: * View subscriptions and payments * Change subscription tier (upgrade or downgrade) * Cancel subscriptions * Update billing details * Download invoices and receipts The Customer Portal uses secure email-based authentication. Consumers authenticate via a verification link sent to their email. *** ## Related Self-service portal for consumers to manage orders and subscriptions. View and manage orders and payment records. # Customer Portal Source: https://docs.waffo.ai/features/customer-portal Self-service portal for consumers to manage orders and subscriptions The Customer Portal gives your consumers a self-service interface to manage their purchases, view invoices, update billing details, and manage subscriptions — no password required. Consumers log in via **Magic Link** — they enter their email, receive a login link, and click to access the portal. Log in to the Consumer Portal to view orders, manage subscriptions, and download invoices. *** ## What Can Customers Do in the Customer Portal? View active subscriptions, next billing date, cancel or reactivate. Browse all past orders and payment history with status details. Download PDF invoices and receipts in 16 languages with full billing details. Update billing address, add business name and tax ID for invoicing. *** ## Portal Access The unified Consumer Portal URL: ``` https://pancake.waffo.ai/consumer/portal/login ``` Consumers enter the email used for their purchase on the login page, receive a one-time Magic Link, and click to access the portal. No password required. *** ## Can't Log In? 1. Make sure you're using the email address from your original purchase (check your receipt email) 2. Check your spam folder for emails from `auth@waffo.ai` 3. Go back to the login page and request a new Magic Link Email `support@waffo.ai` with your purchase email and order details, and we'll help you out. # Finance & Payouts Source: https://docs.waffo.ai/features/finance Track income from payments and request payouts The Finance area covers two related concepts, accessed from different parts of the dashboard: * **Merchant Finance** — your pooled balance across all stores, where you request payouts. * **Store Revenue** — per-store income from successful payments. ## Merchant Finance Open from the merchant menu (top-right avatar → **Merchant Finance**), or at `/merchant/dashboard/finance`. At the top of the page, three KPI cards show your balance at a glance: | Card | Description | | ------------------------- | ---------------------------------------------------------- | | **Available to withdraw** | Net amount that has fully settled and is ready to pay out | | **Processing** | Payments still in the clearing window (≈ 10 business days) | | **Total withdrawals** | Cumulative amount you have paid out so far | Below the cards are two tabs: * **Pay out** — request a payout from your available balance. See [Payouts](/merchant/payout-flow) for the full flow. * **Withdrawal records** — historical payouts and their statuses. If you haven't bound a payout account yet, a banner at the top of the page links to **Payout Accounts**. Read the end-to-end payout flow: from sale to settlement to your bank. ## Store Revenue Each store has its own Revenue page (`/merchant/dashboard/{storeId}/revenue`) showing the income that store has generated. Settled funds from all your stores flow automatically into Merchant Finance — there is no per-store withdrawal. ## Payout Accounts Payout accounts live at the **merchant level** — every store you own shares the same set. Manage them at `/merchant/dashboard/payout-accounts` or via the merchant menu. See [Payout Accounts](/merchant/payout-accounts) for the supported methods, identity prerequisites, and how to add or switch accounts. Receiving payouts requires identity verification. If you haven't completed it yet, the Payout Accounts page shows a Basic Identity card that links to [Identity Verification](/merchant/identity-kyc). # Integrations Source: https://docs.waffo.ai/features/integrations Choose the right integration path for your application ## Choose Your Integration Path **Fastest to ship** Best for teams with an existing codebase that want Claude Code or another coding agent to plan the model, implement the integration, and drive validation. **Most control** Best when you need custom checkout flows, server-side orchestration, dynamic pricing, or tighter control over integration behavior. **Simplest** Best when you want to launch a payment link quickly first and deepen the integration later. *** ## How To Choose | If you care most about | Recommended path | Why | | ------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------- | | Shipping the first usable version quickly | AI Integration | Best when you want a coding agent to turn an existing codebase into a working integration fast | | Full control over payment flow and server logic | API / SDK | Best for custom checkout, dynamic pricing, permissions, and internal orchestration | | Validating the payment funnel before deeper engineering | Hosted Checkout | Best for low engineering overhead and fast launch | Many teams start with Hosted Checkout or AI Integration for the first version, then move deeper into the API / SDK path as requirements grow. *** ## What An Integration Usually Includes Most Waffo Pancake integrations include four parts: * **Authentication** for server-side requests * **Products and checkout** for one-time or subscription billing * **Webhooks** for payment and subscription events * **Environment rollout** from test mode to production ``` Your application | +-- Authentication --> API / SDK requests | +-- Product + Checkout --> Payments | +-- Webhooks --> Order and subscription sync ``` You do not need every layer on day one. Many teams start with hosted checkout or AI-assisted integration, then deepen their API usage later. *** ## What Each Path Usually Delivers ### AI Integration * let an AI coding assistant read `llms-full.txt` and the official skill context * generate integration code, webhook handling, and validation steps for your stack * best when you already have a codebase and want to shorten implementation time ### API / SDK * gives your server full control over products, checkout, webhooks, and state sync * fits dynamic pricing, fine-grained permissions, and custom backend orchestration * best for long-term customization and more complex billing logic ### Hosted Checkout * use dashboard-generated checkout links or public payment entry points directly * best for validating the commercial flow before investing in deeper engineering * usually the best first-stage integration path *** ## Recommended Rollout Order Sign up at [Merchant Dashboard](https://pancake.waffo.ai/merchant/auth/signin) and create your first store. Prepare your Merchant ID and API Key in the merchant dashboard. Define the product type, pricing, and billing interval, then create products via the Dashboard or API. Create checkout sessions through the API, or use Dashboard-generated checkout links. Set up webhook endpoints for payment, refund, and subscription events. Use [Test Mode](/features/test-mode) with test cards to verify the full flow. Publish products and switch to the production environment. *** ## What To Prepare Before Implementation Before writing code, define these clearly: * whether you sell one-time products, subscriptions, or both * whether you need dynamic pricing for overage, credits, or quoted amounts * what access, entitlements, or resources should be granted after payment * which webhook events should drive your business state changes * which capabilities should stay in test mode until production rollout If these decisions are still fuzzy, start with the AI Integration path. If these rules are already well-defined, the API / SDK path is usually the stronger choice. *** ## Core Entry Points Configure Merchant ID, API Key, and server-side authentication. Test the full integration flow without real charges. Receive real-time events and keep orders and subscriptions in sync. Browse official SDKs, framework patterns, and code samples. Use an AI coding assistant for planning, implementation, and validation. Start from the first payment flow and learn the end-to-end path quickly. *** ## Developer Settings The Developers page provides tools for integrating Waffo Pancake with your applications. ## API Keys ### Overview API keys authenticate server-to-server requests to the Waffo Pancake API. * Created for `test` environment * Use for development * No real charges * Separate from production data * Created for `prod` environment * Use for production * Process real payments * Keep private key secure ### Key Types | Key Type | Use Case | Description | | ----------- | ---------------- | ----------------------------------- | | **API Key** | Server-side only | Signed requests for full API access | ### API Key Authentication API Key authentication is handled automatically by the SDK. Install `@waffo/pancake-ts`, provide your Merchant ID and private key, and the SDK will handle request signing automatically. ```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!, }); ``` Never expose your private key in client-side code, version control, or public repositories. ### Creating API Keys Go to Dashboard --> Developers --> API Keys The API Key Generator opens. Click "Generate" to create a key pair. * Public key is sent to the server * Private key stays with you Give it a descriptive nickname (e.g., "Production Server"). Choose **Test** or **Production** environment. **Critical:** Download and securely store your private key. Your private key is only shown once. Store it securely -- you will need it for API authentication. ### Managing Keys | Action | Description | | ------ | -------------------------------------------- | | View | See key nickname, creation date, environment | | Delete | Permanently remove the key | ## Webhooks ### What Are Webhooks? Webhooks notify your server when events occur in Waffo Pancake. ### Available Events | Event | Trigger | | -------------------------------- | ----------------------------------- | | `order.completed` | Order completed | | `subscription.activated` | Subscription activated | | `subscription.payment_succeeded` | Subscription payment succeeded | | `subscription.updated` | Subscription updated | | `subscription.canceling` | Subscription cancellation scheduled | | `subscription.canceled` | Subscription ended | | `subscription.uncanceled` | Subscription cancellation reversed | | `subscription.past_due` | Subscription payment past due | | `refund.succeeded` | Refund processed successfully | | `refund.failed` | Refund processing failed | ### Setting Up Webhooks Enter your webhook URL (must be HTTPS). Choose which events to receive. Save your webhook endpoint. ### Webhook Payload Example ```json theme={"system"} { "event": "order.completed", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "orderId": "660e8400-e29b-41d4-a716-446655440001", "amount": 2900, "currency": "USD", "status": "completed", "createdAt": "2026-01-15T10:30:00.000Z" } } ``` All IDs are UUID v4 format. Amounts are in the smallest currency unit. Timestamps are ISO 8601 UTC. ### Webhook Best Practices Return 2xx status within 30 seconds. Process heavy work asynchronously. Events may be sent multiple times. Use event IDs for deduplication. Failed webhooks retry up to 5 times with increasing delays (5min, 30min, 2h, 24h). Check webhook delivery logs in the Dashboard for failures. ## API Documentation ### Base URL ``` https://api.waffo.ai/v1 ``` ### Architecture Waffo Pancake uses a hybrid API: * **REST (POST)** for all write operations via `/v1/actions/...` * **GraphQL** for all read operations via `/v1/graphql` ### Authentication Example **API Key (Server-to-Server) using SDK:** ```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!, }); const { store } = await client.stores.create({ name: "My Store" }); ``` ### Common Endpoints | Endpoint | Method | Description | | ------------------------------------------------- | ------ | ---------------------------- | | `/v1/actions/onetime-product/create-product` | POST | Create one-time product | | `/v1/actions/subscription-product/create-product` | POST | Create subscription product | | `/v1/actions/onetime-order/create-order` | POST | Create checkout session | | `/v1/actions/subscription-order/create-order` | POST | Create subscription checkout | | `/v1/graphql` | POST | Query data (GraphQL) | Complete endpoint documentation with request/response examples. ## Security Best Practices Store private keys in environment variables, never in code. Regularly rotate keys, especially after team changes. Use different API keys for test and production environments. Store private keys using your platform's secret management. ## Testing ### Test Mode Use test mode for development: * All endpoints work identically * No real charges processed * Test card numbers available * Full webhook testing ### Test Cards | Card Number | Behavior | | --------------------- | -------------------- | | `4576 7500 0000 0110` | Success (Visa) | | `2226 9000 0000 0110` | Success (Mastercard) | | `4576 7500 0000 0220` | Declined | ## Logs and Debugging ### Webhook Logs Track webhook deliveries: * Event type * Delivery status (success/failed) * HTTP response from your endpoint * Retry attempts and timestamps # Notifications Source: https://docs.waffo.ai/features/notifications Stay informed with real-time activity alerts ## Notification Center The Notification Center keeps you informed about important events in your Waffo Pancake account. Access it from the bell icon in the Dashboard header. *** ## Notification Types * New payments received * Payment failures * Refund requests * Payout notifications * New subscriptions * Cancellations * Trial expirations * Renewal failures * New customer signups * Customer updates * Portal access * Payout processed * API key events * Security alerts * Feature announcements *** ## Notification Panel ### Accessing Notifications 1. Click the bell icon in the Dashboard header 2. View unread count on the badge 3. Browse recent notifications ### Notification Content Each notification includes: | Element | Description | | ----------- | --------------------------------------------- | | **Icon** | Visual indicator of notification type | | **Title** | Brief description of the event | | **Details** | Relevant information (amount, customer, etc.) | | **Time** | When the event occurred | | **Actions** | Quick actions (view details, mark read) | *** ## Notification Page Access the full Notifications page from **Dashboard → Notifications** for: * Complete notification history * Advanced filtering * Bulk actions ### Filtering Options | Filter | Options | | -------------- | ------------------------------------------ | | **Type** | Payments, Subscriptions, Customers, System | | **Status** | Unread, Read, All | | **Date Range** | Today, This Week, This Month, Custom | ### Bulk Actions * Mark all as read * Clear old notifications * Export notification history *** ## Notification Settings Configure which notifications you receive in **Settings --> Notifications**. ### Customer Email Notifications Control which emails are sent to your customers: | Setting | Description | | ------------------------- | ------------------------------------------------------ | | Order confirmation | Email customers when they complete a one-time purchase | | Subscription confirmation | Email customers when they start a new subscription | | Subscription cycled | Email customers when their subscription renews | | Subscription canceled | Email customers when their subscription is canceled | | Subscription revoked | Email customers when their subscription is revoked | | Subscription past due | Email customers when their subscription payment fails | ### Merchant Email Notifications Control which emails you receive as a merchant: | Setting | Description | | ------------------------- | -------------------------------------------------------------- | | Order notification | Receive an email when a customer completes a one-time purchase | | Subscription notification | Receive an email when a customer starts a new subscription | *** ## Real-Time Updates Notifications appear in real-time without page refresh via the notification panel in your Dashboard. *** ## Notification Actions ### Quick Actions From the notification panel: * **View** — Go to related item (payment, customer, etc.) * **Mark Read** — Dismiss the notification * **Dismiss** — Remove from panel ### Detailed View Click on a notification to see full details: * Complete event information * Related customer data * Transaction details * Available actions *** ## Security Notifications Security notifications cannot be disabled. They alert you to important security events. Security alerts include: * New device sign-in * API key created/deleted * Unusual activity ### Recommended Actions When you receive a security alert: 1. Review the activity immediately 2. Verify it was authorized 3. If suspicious, revoke any compromised API keys and sign out of all sessions 4. Contact support if needed *** ## Best Practices Enable instant notifications for payment failures to quickly resolve issues. For high-volume stores, use daily email digests instead of instant notifications. Always investigate security notifications promptly. Keep your notification panel clean by marking items as read. *** ## Notification History Access historical notifications from the Notifications page in your Dashboard. # Orders & Payments Source: https://docs.waffo.ai/features/orders-payments Track transactions and manage your payment lifecycle ## Overview Every purchase in Waffo Pancake creates an **order** and an associated **payment** record. Orders represent the customer's intent to buy, while payments track the actual money movement. *** ## Order Statuses Orders have different status sets depending on the product type. ### One-Time Orders | Status | Description | | ---------- | ------------------------------------ | | `pending` | Order created, awaiting payment | | `paid` | Payment succeeded, order fulfilled | | `canceled` | Order was canceled before completion | ### Subscription Orders | Status | Description | | ----------- | --------------------------------------------------------- | | `pending` | Subscription created, awaiting first payment | | `active` | Subscription is live and billing normally | | `trialing` | Customer is in a free trial period | | `past_due` | Payment failed, retrying | | `canceling` | Cancellation requested, active until period end | | `canceled` | Subscription canceled (access continues until period end) | | `expired` | Subscription expired | | `closed` | Never activated — payment timed out | *** ## Payment Statuses Payment initiated, awaiting processing. Payment is being processed. Payment completed successfully. Payment failed during processing. Payment was canceled (timeout, merchant action, or buyer cancellation before processing). Full refund has been processed. A partial refund has been processed. `processing` may appear in the Dashboard UI but is not returned by the API or GraphQL. Payment statuses from the API are: `pending`, `succeeded`, `failed`, `canceled`. Refund status is tracked separately via the `refundStatus` field on Payment (`none` / `pending` / `refunded` / `failed`), not as a payment status value. *** ## Payment List View all payments in a table with the following columns: | Column | Description | | -------------- | ----------------------------------------- | | Date | Transaction timestamp | | Amount | Payment amount as a display format string | | Tax Amount | Tax collected on the transaction | | Status | Current payment status | | Payment Method | `card`, `bank_transfer`, or `wallet` | | Customer | Customer email address | | Currency | ISO 4217 currency code | ### Filtering | Filter | Options | | ---------- | -------------------------------------------------------------------------------- | | Status | `pending`, `processing`, `succeeded`, `failed`, `refunded`, `partially_refunded` | | Date Range | Custom start and end dates | *** ## Payment Details Click any payment to view its full record. ### Transaction Info | Field | Description | | ---------- | -------------------------------------------- | | Payment ID | UUID v4 identifier | | Order ID | Associated order | | Store ID | Store that received the payment | | Amount | Gross payment amount (display format string) | | Currency | ISO 4217 currency code | | Status | Current payment status | | Created At | ISO 8601 timestamp | | Updated At | ISO 8601 timestamp | ### Amount Details | Field | Description | | -------------------- | ----------------------------------------------------- | | `amount` | Total charged amount | | `taxAmount` | Tax portion of the amount | | `settlementCurrency` | Currency used for settlement | | `settlementAmount` | Amount in settlement currency (display format string) | | `refundedAmount` | Total amount refunded so far | ### Billing Detail | Field | Description | | -------------- | ----------------------------------- | | `country` | Customer's billing country | | `state` | Billing state or region | | `postcode` | Billing postal code | | `isBusiness` | Whether this is a business purchase | | `businessName` | Business name (if applicable) | | `taxId` | Tax ID (if applicable) | ### Payment Method Payments record which method was used: | Method | Value | | ----------------- | --------------- | | Credit/Debit Card | `card` | | Bank Transfer | `bank_transfer` | | Digital Wallet | `wallet` | Additional method-specific details may be available in the `paymentMethodDetails` field. The structure of this field varies by payment method. *** ## Supported Payment Methods Credit and debit card payments. Direct bank-to-bank transfers. Digital wallet payments (Apple Pay, Google Pay, etc.). *** ## Refunds Refund requests are handled through a separate ticket-based workflow. Buyers submit a refund ticket specifying the payment and reason, and merchants review and approve or reject the request. For full details on the refund process, statuses, and policies, see the [Refunds](/customers/refunds) page. **Key rules:** * One-time product refunds must be requested within **14 days** of payment * Subscription cancellations take effect at the end of the current billing period * Refund tickets track their own status: `pending`, `approved`, `rejected`, `processing`, `succeeded`, `failed` *** ## API Reference ### Creating Orders Orders are created through a two-step checkout session flow: **Step 1: Create a checkout session** (API Key or Store Slug auth) ```bash One-Time Product theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "storeId": "store-uuid", "productId": "product-uuid", "productType": "onetime", "currency": "USD" }' ``` ```bash Subscription Product theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "storeId": "store-uuid", "productId": "product-uuid", "productType": "subscription", "currency": "USD" }' ``` This returns a `sessionId`, `checkoutUrl`, and `expiresAt`. The session locks the product version and price snapshot for 7 days. **Step 2: Create the order** (API Key auth) ```bash One-Time Order theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/onetime-order/create-order \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "checkoutSessionId": "session-uuid", "billingDetail": { "country": "US", "isBusiness": false, "state": "CA" } }' ``` ```bash Subscription Order theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/subscription-order/create-order \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "checkoutSessionId": "session-uuid", "billingDetail": { "country": "US", "isBusiness": false, "state": "CA" } }' ``` Both endpoints return a `checkoutUrl` that the buyer should be redirected to for payment. ### Querying Payments Use the GraphQL endpoint to query payment records: ```graphql theme={"system"} query { payments(storeId: "store-uuid", limit: 20) { id orderId amount currency status paymentMethod amountDetails { amount taxAmount settlementCurrency settlementAmount refundedAmount } billingDetail { country state postcode isBusiness businessName taxId } createdAt } } ``` All amounts are display format strings. For example, `"29.00"` in USD means \$29.00. For zero-decimal currencies like JPY, `"4500"` means 4500 yen. # Products Source: https://docs.waffo.ai/features/products One-time or recurring. Your revenue, your rules. ## Product = Billing Object In Waffo Pancake, a **product** is the object used for checkout, taxation, and reporting. It is the commercial offer you charge for, not just a storefront card. For professional use, think of products as billing entities: * a fixed-price offer * a subscription plan * or a base product used with dynamic pricing at checkout *** ## Core Models Customer pays once for a predefined offer. * Digital downloads * Lifetime licenses * Templates, courses, assets Customer is billed on a recurring schedule. * SaaS plans * Memberships * Service retainers Amount is calculated at checkout time. * Usage overage * Credit top-ups * Negotiated quotes *** ## Create a Product Dashboard --> Products --> **Create Product** ```json theme={"system"} { "storeId": "your-store-uuid", "name": "Pro Plan", "prices": { "USD": { "amount": "29.00", "taxIncluded": false, "taxCategory": "saas" } } } ``` **Done.** You get a permanent purchase link for fixed-price selling, or a reusable billing object for programmatic checkout. *** ## Product Fields ### Required | Field | What It Is | | ------------ | ----------------------------------- | | Name | What customers see | | Price | How much you charge | | Currency | USD, EUR, GBP, CNY, JPY, HKD, etc. | | Type | One-time or Subscription | | Tax Category | SaaS, Digital Goods, Software, etc. | ### Optional | Field | Default | Purpose | | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------- | | Description | — | Markdown. Sell your product. | | Image | — | 400x400px recommended | | Redirect URL | — | Where the customer lands after checkout. Used for both successful and failed payments — only one URL is configurable. | ### Price Limits Each currency has a minimum and maximum price you can set on a product. Values outside this range are rejected when you save the product. | Currency | Minimum | Maximum | | -------- | -------: | ------------: | | USD | \$1.00 | \$7,500.00 | | EUR | €1.00 | €7,400.00 | | GBP | £1.00 | £6,400.00 | | JPY | ¥100 | ¥1,250,000 | | HKD | HK\$8.00 | HK\$61,000.00 | CNY is available for display in your dashboard analytics, but cannot currently be used as a product pricing currency. *** ## Dynamic Pricing Dynamic pricing is the right model when the final amount is not known until runtime. Examples: * usage-based overage billing * prepaid credit packs with variable size * contract-specific quotes * temporary discounts calculated on your server ### How It Works 1. Create a **base one-time product** in Waffo Pancake 2. Calculate the final amount on your server 3. Pass `priceSnapshot` when creating the checkout session ```typescript theme={"system"} const session = await client.checkout.createSession({ storeId: "store_id", productId: "usage-overage-product-id", productType: "onetime", currency: "USD", priceSnapshot: { amount: calculatedAmount, taxIncluded: false, taxCategory: "saas", }, }); ``` `priceSnapshot` overrides the stored product price for that checkout session. Use it only from your server, never from untrusted client code. If the amount is event-based, keep it as a one-time charge even if the customer is already on a subscription. *** ## Free Trials Let customers try subscription products before they buy. ### Trial Abuse Protection Waffo Pancake automatically tracks consumer trial history at the platform level. When a consumer starts a new subscription, the platform calculates the maximum available trial days based on their history — preventing repeated trial abuse without any effort from the merchant. Merchants can also pass `requested_trial_days` via the API to shorten or skip the trial for specific customers. Effective trial abuse prevention requires `buyerIdentity`. Use [authenticated checkout](/integrate/sdks#authenticated-checkout-recommended) to ensure each buyer can only claim one trial per product. *** ## Billing Intervals | Interval | When | Best For | | --------- | --------------- | ------------------- | | Weekly | Every 7 days | High-usage products | | Monthly | Every month | Standard SaaS | | Quarterly | Every 3 months | B2B software | | Yearly | Every 12 months | Committed customers | Annual plans with 15-20% discount = lower churn + better cash flow. *** ## Product Purchase Links Every product gets a permanent purchase link that never expires: ``` https://checkout.waffo.ai/{store-slug}/{product-slug} ``` Test mode: ``` https://checkout.waffo.ai/{store-slug}/{product-slug}/test ``` **Add to your website:** * Website button * Twitter bio * Email signature * Discord server **Key benefits:** * Link is permanent — it stays the same even when you update product details * No session or token management needed * A checkout session is created automatically when the customer opens the link **Getting your link:** * **Dashboard**: Click "Copy Link" on any product * **API**: For advanced use cases, create a Checkout Session via API with custom parameters (dynamic link, 7-day TTL) **Test vs Live:** * Toggle Test Mode in Dashboard header to preview with test links * Test links use the `/test` suffix *** ## Product Status Live. Customers can buy. Hidden. Existing subscriptions continue. *** ## Updating Products Products use **immutable versioning**: Updates create new versions. Existing subscriptions keep their original version. | Field | Impact | | ---------------- | ---------------------------- | | Name/Description | Creates new version | | Image | Creates new version | | Price | New purchases only | | Interval | Cannot change after creation | ### Environment Sync Publish product versions from test to production: ```bash theme={"system"} POST /v1/actions/onetime-product/publish-product { "id": "product-id" } ``` Publishing is one-way (test → prod) and only needed for the first publish. *** ## Best Practices Include billing period: * "Pro Plan - Monthly" * "Pro Plan - Annual (Save 20%)" * Round numbers ($29, not $28.73) * Annual discount (15-20%) * Research competitors * 400x400px minimum * PNG or JPG * Under 2MB # Refunds Source: https://docs.waffo.ai/features/refunds Manage refund tickets, process full or partial refunds, and track refund status ## Overview Waffo Pancake uses a **refund ticket** system. Buyers request refunds through the API or Customer Portal, and each request creates a ticket that moves through a defined lifecycle. Merchants review and resolve tickets from the dashboard. *** ## Business Rules These rules are enforced by the API and cannot be overridden. * **One-time products** are eligible for refund within **14 days** of payment. * **Subscriptions** do not use the refund ticket system. Cancellation takes effect at the end of the current billing period. * **Partial refunds** are supported by providing a custom `amount` (as a display format string, e.g., "15.00") when creating a ticket. *** ## Refund Ticket Statuses Every refund ticket moves through the following lifecycle: Refund requested, awaiting review. Refund approved by the merchant. Refund request denied. Refund is being processed by the payment provider. Refund completed successfully. Refund processing failed. *** ## Creating a Refund Ticket Refund tickets are created by calling the API with API Key authentication. ### Endpoint ``` POST /v1/actions/refund-ticket/create-ticket ``` **Authentication:** API Key ### Request Body The ID of the payment to refund (UUID v4). A description of why the refund is being requested. Amount to refund as a display format string (e.g., "15.00"). Omit for a full refund. ### Response The unique ID of the created refund ticket. Initial status of the ticket. Always `pending` on creation. The refund amount as a display format string. ### Example ```bash cURL theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/refund-ticket/create-ticket \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "paymentId": "550e8400-e29b-41d4-a716-446655440000", "reason": "Product not as described", "amount": "15.00" }' ``` ```typescript SDK theme={"system"} import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); const { ticketId, status, requestedAmount } = await client.refunds.createTicket({ paymentId: "550e8400-e29b-41d4-a716-446655440000", reason: "Product not as described", amount: "15.00", }); ``` *** ## Querying Refund Tickets Use GraphQL to retrieve refund ticket data. ```graphql theme={"system"} query { refundTickets(storeId: "STORE_ID") { id paymentId storeId customerId reason status requestedAmount approvedAmount currency createdAt updatedAt resolvedAt } } ``` | Field | Type | Description | | ----------------- | ------ | -------------------------------------------------- | | `id` | string | Refund ticket ID | | `paymentId` | string | Associated payment ID | | `storeId` | string | Store that owns the payment | | `customerId` | string | Buyer who requested the refund | | `reason` | string | Reason provided by the buyer | | `status` | string | Current ticket status | | `requestedAmount` | string | Amount the buyer requested (display format string) | | `approvedAmount` | string | Amount approved (may differ from requested) | | `currency` | string | ISO 4217 currency code | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp | | `resolvedAt` | string | ISO 8601 timestamp (null if unresolved) | *** ## Webhook Notifications You can configure webhooks to receive notifications when refund ticket events occur. See the [Webhooks guide](/integrate/webhooks) for setup instructions. *** ## Reducing Refund Requests Accurate descriptions reduce "not as expected" refund requests. Let customers try before buying to reduce post-purchase regret. Address issues quickly before they become refund requests. Use a clear billing descriptor so customers recognize charges. # Subscriptions Source: https://docs.waffo.ai/features/subscriptions Recurring revenue on autopilot ## Recurring Revenue. Automated. Customers subscribe. We handle billing cycles, renewal recovery, and lifecycle management. You focus on your product. *** ## How It Works ``` Customer subscribes → Billing cycle → Auto-charge → Repeat ``` If a renewal charge is not collected successfully, the subscription enters `past_due`. This is a recovery state, not an immediate termination state. The system can keep the subscription in a grace or retry window while the customer updates payment details. *** ## Subscription Plan Structure In Waffo Pancake, each **separately purchasable subscription option** is an independent subscription product. The most common distinction is billing interval, such as: * monthly * yearly If you offer both monthly and yearly billing, you would normally create **two subscription products**. | Business offer | Waffo model | | -------------- | ---------------------- | | Monthly plan | 1 subscription product | | Yearly plan | 1 subscription product | *** ## Subscription States | Status | Description | | ----------- | ------------------------------------------------------------------ | | `pending` | Awaiting first payment | | `active` | Live and billing normally | | `trialing` | In free trial period | | `past_due` | Renewal charge not collected, in recovery or grace period | | `canceling` | Cancellation requested, access continues until current period ends | | `canceled` | Will not renew after current period | | `expired` | Subscription has reached the end of its term | | `closed` | Never activated — payment timed out | *** ## Billing Intervals | Interval | Frequency | Best For | | --------- | --------------- | -------------------- | | Weekly | Every 7 days | Usage-heavy products | | Monthly | Every month | Standard SaaS | | Quarterly | Every 3 months | B2B software | | Yearly | Every 12 months | Committed customers | *** ## Free Trials Reduce signup friction. Let customers try before they commit. ### Configure Trials When creating a subscription product, enable the trial toggle and set the number of days in the Dashboard. ### Platform-Level Trial Protection Waffo Pancake acts as the Merchant of Record and automatically prevents trial abuse: | Layer | How It Works | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **Platform (Pancake)** | Tracks each consumer's trial history across all merchants. Calculates the maximum available trial days for each new subscription. | | **Merchant** | Can pass `requested_trial_days` when creating an order via API to customize the trial length per customer. | **How the two layers interact:** * If the merchant requests ≤ the platform maximum → the merchant's requested value is used * If the merchant requests > the platform maximum → falls back to the platform maximum * If the merchant doesn't specify → the full platform maximum is used ### Buyer Identity & Trial Protection Trial eligibility is tracked by `buyerIdentity` — a stable identifier you provide via [authenticated checkout](/integrate/sdks#authenticated-checkout-recommended). The platform uses this to detect repeat trial claims across sessions. Without `buyerIdentity` (anonymous checkout), trial eligibility checks are skipped entirely. Any buyer can claim unlimited trials by changing their email address. 7-14 day trials work best. Too short = not enough time to evaluate. Too long = forgotten. *** ## Renewal Recovery When a renewal charge is not collected successfully, the subscription transitions to `past_due`. This usually means the subscription is in a retry or grace window while the customer updates their payment method, not that access is terminated immediately. Common handling includes: * notifying the customer to update payment details * keeping access active during a grace period * retrying collection based on the dunning policy Treat `past_due` as "renewal requires recovery", not simply "payment failed and service stopped". *** ## Managing Subscriptions ### Cancel Cancellation is always effective at the end of the current billing period. When a cancellation is requested, the subscription enters the `canceling` intermediate state. The customer retains access until the current billing period ends, at which point the status transitions to `canceled`. ```bash theme={"system"} POST /v1/actions/subscription-order/cancel-order { "orderId": "ORD_5dXBtmF2HLlHfbPNm0Wcnz" } ``` Response includes `currentPeriodEnd` so you know when access expires. The returned status will be `canceling` (or `canceled` if the period has already ended). There is no immediate cancellation option. Customers always retain access through the end of their paid period. The `canceling` → `canceled` transition happens automatically when the current billing period expires. ### Resume Subscription If a subscription is still in `canceling`, the customer can resume it before the current period ends. After resuming: * the subscription returns to `active` * access continues without interruption * future renewals continue on the original billing cycle Resuming applies to subscriptions that were canceled but have not reached the end of the current period yet. It is effectively an undo for cancellation, not a brand-new purchase. ### Upgrade/Downgrade | Scenario | Behavior | | ------------------- | ----------------------------------------------------- | | Upgrade mid-cycle | Takes effect immediately | | Downgrade mid-cycle | Takes effect at the end of the current billing period | ```bash theme={"system"} POST /v1/actions/subscription-order/change-product { "orderId": "550e8400-e29b-41d4-a716-446655440000", "targetProductId": "target-product-uuid" } ``` This endpoint currently returns 501 (Not Implemented). Upgrade/downgrade functionality is planned but not yet available. ## Metrics ### MRR (Monthly Recurring Revenue) ``` Weekly subscription: $10 × 4.33 = $43.30 MRR Monthly subscription: $29 × 1 = $29 MRR Annual subscription: $290 ÷ 12 = $24.17 MRR ``` ### Key Metrics | Metric | What It Tells You | | ------ | ------------------------- | | MRR | Monthly recurring revenue | | Churn | % subscriptions canceled | | LTV | Customer lifetime value | | ARPU | Average revenue per user | *** ## Webhooks Subscribe to subscription lifecycle events via webhooks. Configure webhook endpoints in **Settings --> Webhooks**. Specific webhook event names are not listed here as they may change. Refer to the webhook configuration in your Dashboard for the current list of available events. Webhook payloads use standard Waffo Pancake conventions: * IDs are UUID v4 format * Amounts as display format strings * Timestamps in ISO 8601 UTC * Billing frequency uses the `billingPeriod` field (e.g., `monthly`, `yearly`) *** ## Customer Portal Let customers manage their own subscriptions: * View details * Update payment method * Change plans * Cancel * Resume subscription * Download invoices Self-service subscription management. *** ## Best Practices 15-20% off yearly = lower churn + better cash flow. A missed renewal charge does not need to mean instant cancellation. Give the customer time to recover the subscription. Trial ending. Upcoming charge. No surprises. # Test Mode Source: https://docs.waffo.ai/features/test-mode Break things. Risk-free. ## Test Everything. Break Nothing. Build and test your entire integration without real transactions. No real charges. No real payouts. Just safe experimentation. No real money moves. Everything works like Live Mode. *** ## Test vs Live | | Test Mode | Live Mode | | ------------ | ---------------- | ---------------- | | Transactions | Simulated | Real charges | | Money | Fake | Real funds | | Data | Isolated | Production | | Webhooks | Fully functional | Fully functional | | API Behavior | Identical | Identical | **Switch modes:** Dashboard header toggle or `X-Environment` header in API calls. | Header Value | Mode | | --------------------- | --------- | | `X-Environment: test` | Test Mode | | `X-Environment: prod` | Live Mode | Always verify you're in the correct mode before sharing payment links. *** ## API Keys by Environment API Keys are created for a specific environment (test or production). When creating an API key in the Dashboard, you select which environment the key belongs to. | Environment | Purpose | | ----------- | ----------------------- | | Test | Development and testing | | Production | Real payments | API Keys use key pair authentication, not prefixed secret keys. See [Authentication](/api-reference/authentication) for details. *** ## Test Cards ### Successful Payments | Card | Type | | --------------------- | ----------------- | | `4576 7500 0000 0110` | Visa Credit | | `2226 9000 0000 0110` | Mastercard Credit | | `4001 7000 0000 0110` | Visa Debit | | `2226 9300 0000 0110` | Mastercard Debit | ### Declined Payments | Card | Type | | --------------------- | ----------------- | | `4576 7500 0000 0220` | Visa Credit | | `2226 9000 0000 0220` | Mastercard Credit | | `4001 7000 0000 0220` | Visa Debit | | `2226 9300 0000 0220` | Mastercard Debit | Any future expiry date. Any 3-digit CVC. *** ## Test Payment Methods ### Digital Wallets Test Mode shows simulated UI: * **Apple Pay** -- Safari/iOS * **Google Pay** -- Chrome/Android *** ## Test Webhooks Webhooks fire normally in Test Mode. To test your webhook endpoints: 1. Register your webhook endpoint in Dashboard --> Developers 2. Switch to Test Mode 3. Perform actions that trigger events (create orders, complete payments) 4. Your endpoint receives webhook events just like in Live Mode *** ## Test Subscriptions Test the full lifecycle: 1. **Create** -- Subscribe via test checkout 2. **Bill** -- Billing occurs on schedule (use short intervals for faster testing) 3. **Update** -- Change plans via the Customer Portal or API 4. **Cancel** -- Test cancellation flow 5. **Expire** -- Verify expired subscription behavior *** ## Test Data ### Isolation Test data is completely separate: * Test products don't appear in Live Mode * Test customers are separate * Test transactions don't affect live reports ### Environment Sync When ready to go live, sync your products from test to production: ```bash theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/onetime-product/publish-product \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY_TOKEN" \ -d '{ "id": "product-uuid" }' ``` The publish-product endpoint is a one-way operation from test to production. It does not require an `X-Environment` header. *** ## Going Live Checklist Before accepting real payments: * [ ] Complete end-to-end testing in test mode * [ ] Test edge cases (declined cards, failures) * [ ] Verify webhook handling * [ ] Test checkout on mobile devices * [ ] Add a payout account (Payout Accounts) * [ ] Complete business details (Settings --> Business Details) * [ ] Review product pricing * [ ] Sync products from test to production using publish-product Full going-live checklist. # AI Product Billing Source: https://docs.waffo.ai/guides/ai-billing Set up flexible billing for AI products and API services ## Who Is This For? This guide is for developers building AI products, API services, or any usage-based business. Common examples: | Business Type | Examples | | ------------------ | --------------------------------------------------------- | | AI API Services | LLM API proxies, image generation, speech recognition | | AI SaaS | AI writing assistants, AI code completion, AI translation | | AI Agent Platforms | Automated workflows, data analysis agents | | API Services | Payment processing, messaging, data queries | *** ## How Billing Works with Waffo Pancake Waffo Pancake does not provide built-in usage metering, but you can model AI billing with two primary approaches: `subscriptions` and `on-demand purchases`. In many businesses, these two approaches work best together. ### Approach 1: Subscriptions Create subscription products at different tiers, each with a different token quota, request allowance, or task limit. > Prices below are examples only. Set them based on your actual business and costs. | Plan | Monthly | Token Quota | Best For | | ----------- | ----------- | ----------- | --------------------- | | **Starter** | \$9/month | 100K tokens | Individual developers | | **Pro** | \$49/month | 1M tokens | Small teams | | **Scale** | \$199/month | 10M tokens | Enterprise | Best for: * recurring monthly or annual access * plans with included usage * upgrade paths that unlock higher quotas ### Approach 2: On-Demand Purchases Use one-time orders or dynamic pricing to charge based on additional usage, credits, or runtime-calculated amounts. Best for: * buying extra tokens or credits * charging for overage beyond the included plan * per-task, per-call, or quoted usage #### Dynamic Pricing via Checkout Session Pass dynamic pricing through `priceSnapshot` when creating a checkout session to charge based on actual usage. ```typescript theme={"system"} const session = await client.checkout.createSession({ storeId: "store_id", productId: "usage-product-id", productType: "onetime", currency: "USD", // replace with your currency priceSnapshot: { amount: calculatedAmount, // dynamically calculated based on usage taxIncluded: false, taxCategory: "saas", }, }); ``` ### External Metering Tools Use dedicated metering tools to track usage, combined with Waffo Pancake for billing. *** ## Recommended Metering Tools API aggregation and management platform with multi-model key distribution, usage tracking, and quota control. Waffo Pancake partner. *** ## Recommended Combination Most AI products do not need to choose only one billing model. The most common pattern is to combine subscriptions with on-demand purchases. ### Combined Pattern: Base Subscription + On-Demand Usage This is the most common and easiest billing model for customers to understand. **Setup:** 1. Create tiered subscription products such as Starter / Pro / Scale 2. Include a fixed quota of tokens, requests, or tasks in each plan 3. Track actual usage with an external metering system 4. When a user exceeds the included quota, charge extra through a one-time order or `priceSnapshot` **Fits well for:** * AI API services * AI SaaS * AI agent platforms **Dashboard workflow:** * Monitor active plans in **Subscriptions** * Review extra purchases or overage charges in **Payments** * Track subscription revenue and on-demand revenue together in **Analytics** *** ## Key Webhook Events | Event | Action | | ------------------------ | --------------------------------------------------- | | `subscription.activated` | Set initial token/request quota | | `subscription.updated` | Adjust quota to match new plan | | `subscription.canceled` | Revoke API access or downgrade to free tier | | `subscription.past_due` | Reduce quota or throttle API calls | | `order.completed` | Grant additional token allowance (overage purchase) | For API details, see the [API Reference](/api-reference/introduction). *** ## Testing Set up both subscription products and on-demand products in test mode. Use `4576 7500 0000 0110` to subscribe to each tier. Test your usage tracking and quota enforcement logic. Verify the dynamic pricing and on-demand billing flow. *** ## Launch Checklist * [ ] Subscription products created for each tier * [ ] On-demand or overage product created * [ ] Pricing and quotas clearly documented * [ ] Usage tracking/metering tool integrated * [ ] Quota enforcement working correctly * [ ] Upgrade/downgrade flows tested * [ ] On-demand billing flow verified * [ ] Products published to production *** ## Next Steps Learn more about subscription management Automate quota management with webhooks # Guide Source: https://docs.waffo.ai/guides/introduction Step-by-step guides for common business scenarios, from product setup to daily operations ## Find the Right Guide for Your Business Whether you're selling digital products, running a SaaS subscription, or managing day-to-day operations, these guides walk you through the entire process — from setting up in the Dashboard to going live. *** ## Business Scenarios Courses, templates, ebooks, software licenses. Set up one-time purchases and start selling in minutes. Monthly/yearly plans with trials and cancellation handling. Manage recurring revenue with ease. Billing for AI products and API services. Subscription tiers with token quotas, dynamic pricing, and metering tools. *** ## Operations Get notified when payments succeed, subscriptions renew, or refunds are processed. Configure notifications in Dashboard. Handle refund requests, set refund policies, and track refund status from your Dashboard. *** ## Quick Decision Guide | I want to... | Guide | | --------------------------------------- | -------------------------------------------------- | | Sell a course, ebook, or template | [Sell Digital Products](/guides/one-time-payments) | | Launch a SaaS with monthly/yearly plans | [Run SaaS Subscriptions](/guides/subscriptions) | | Bill for AI products or API usage | [AI Product Billing](/guides/ai-billing) | | Get notified on payment events | [Set Up Webhooks](/guides/webhooks) | | Handle customer refund requests | [Manage Refunds](/guides/refunds) | *** ## Before You Start Make sure you have: 1. **A Waffo Pancake account** — [Sign up here](https://pancake.waffo.ai/merchant/auth/signin) 2. **A store created** — You'll be guided through this during onboarding 3. **Test mode enabled** — All guides use test mode by default, so you can safely experiment You can do everything from the Dashboard without writing any code. When you're ready for deeper integration, check the [API Reference](/api-reference/introduction). *** ## No-Code vs Code ### No-Code (Recommended to start) 1. Create products in Dashboard 2. Copy the checkout link 3. Share anywhere — your website, email, social media **Best for:** Quick launches, validating ideas, non-technical merchants. ### Code Integration 1. Create checkout sessions via API 2. Handle webhooks for automated fulfillment 3. Build custom flows **Best for:** Custom experiences, automated delivery, advanced tracking. All guides start with the no-code approach. Code integration is introduced where it adds value, with links to the API Reference for details. # Sell Digital Products Source: https://docs.waffo.ai/guides/one-time-payments Set up one-time purchases for courses, templates, ebooks, and software licenses ## Who Is This For? This guide is for merchants who sell digital products with a single payment — no recurring billing. Common examples include: | Product Type | Examples | | ----------------- | ---------------------------------------------- | | Digital Downloads | Ebooks, templates, design assets, stock photos | | Online Courses | Video courses, tutorials, workshops | | Software Licenses | Desktop apps, plugins, browser extensions | | Digital Services | One-time consultations, audits, reports | *** ## Step 1: Create Your Product In your Dashboard, navigate to **Products** from the sidebar. Products list page Click the **Create Product** button in the top right corner. Select **One-Time** as the product type. Create one-time product form * **Name**: Your product name * **Price**: Set the price and currency * **Description**: What the customer gets * **Image**: Upload a product image (optional but recommended) Click **Save** to create the product. The product starts in **test mode** — only visible to test checkouts. When you're ready to sell, switch the product status to **Active** in the product detail page. *** ## Step 2: Share Your Checkout Link Once your product is created, you'll get a checkout link that you can share anywhere. ### Get the Link On the product detail page, copy the checkout link: ``` https://checkout.waffo.ai/your-store/my-product ``` ### Where to Share * **Your website** — Add a "Buy Now" button linking to the checkout URL * **Email campaigns** — Include the link in newsletters or launch emails * **Social media** — Share directly on Twitter, Instagram, or other platforms * **Landing pages** — Embed the link in your marketing pages This is all you need to start selling. No code required. Customers click the link, pay, and receive a confirmation email automatically. *** ## Step 3: Track Orders After customers purchase, you can track everything from the Dashboard. ### Orders Page Navigate to **Payments** in the sidebar to see all completed orders. Payments list page Each order shows: * Customer email * Amount paid * Payment method * Order status * Date and time ### Email Notifications By default, both you and the customer receive email notifications on purchase. You can customize notification settings in **Settings → Notifications**. *** ## Step 4: Manage Your Products ### Update Product Details You can update the product name, description, price, or image at any time from the product detail page. When you update a product, Waffo Pancake creates a new version. Existing customers who already purchased are not affected. ### Publish to Production Products you create in test mode need to be published before real customers can purchase them: 1. Go to the product detail page 2. Click **Publish to Production** 3. The product is now live and available for real payments *** ## Business Scenarios The following are example scenarios. Adjust them to match your actual business. ### Scenario 1: Selling an Online Course 1. **Create product**: Enter your course name and set a price of \$49 (example) 2. **Share link**: Add the checkout link to your course landing page 3. **Customer purchases**: They pay and receive an order confirmation email 4. **Deliver content**: Use webhooks to automatically grant course access, or manually send access details ### Scenario 2: Selling Design Templates 1. **Create product**: Upload your template bundle with preview images 2. **Set pricing tiers**: Create separate products for different bundles (e.g., Basic $19, Pro $49, Complete \$99) (example) 3. **Share links**: Each product has its own checkout link 4. **Delivery**: Configure webhooks to send download links after purchase ### Scenario 3: Software Licenses 1. **Create product**: Set up your software as a one-time purchase 2. **Customer purchases**: Payment is processed automatically 3. **Generate license**: Use webhooks to trigger license key generation in your system 4. **Deliver**: Customer receives their license key via email *** ## Going Further: Code Integration For automated delivery or custom checkout flows, you can integrate with the Waffo Pancake API. ### When You Need Code * **Automated delivery**: Send download links or license keys automatically after payment * **Custom tracking**: Pass metadata (user ID, campaign source) to track conversions * **Dynamic pricing**: Create checkout sessions with custom amounts ### How It Works ``` Customer clicks "Buy" → Checkout Session → Payment → Webhook → Your server delivers the product ``` For API details, see the [API Reference](/api-reference/introduction). *** ## Testing Before Launch Use test mode to verify your entire flow before accepting real payments. Toggle the **Test/Production** switch in the top navigation bar of your Dashboard. Open your checkout link and complete a purchase using a test card: | Scenario | Card Number | | ------------------------------- | --------------------- | | Successful payment (Visa) | `4576 7500 0000 0110` | | Successful payment (Mastercard) | `2226 9000 0000 0110` | | Declined | `4576 7500 0000 0220` | Check that the test order appears in your **Payments** page and that notification emails were sent. *** ## Launch Checklist Before going live: * [ ] Product created with correct name, description, and price * [ ] Checkout link tested in test mode * [ ] Order confirmation email looks good * [ ] Product published to production * [ ] Checkout link shared on your website / social media *** ## Next Steps Automate delivery by receiving payment notifications Handle refund requests from customers # Manage Refunds Source: https://docs.waffo.ai/guides/refunds Handle refund requests, set refund policies, and track refund status ## What You'll Learn How to manage the refund process from your Dashboard: * Review and process refund requests * Understand refund types and policies * Track refund status * Handle subscription cancellations with refunds *** ## Refund Types | Type | Description | When to Use | | ------------------ | -------------------------------- | ----------------------------------------------- | | **Full Refund** | Refund the entire payment amount | Customer dissatisfied, product not as described | | **Partial Refund** | Refund part of the payment | Partial service delivered, goodwill gesture | *** ## Step 1: Review Refund Requests Customers can submit refund requests through the Customer Portal. These appear in your Dashboard. Navigate to **Payments** in the sidebar to see all payments, including those with refund requests. Payments page with refund requests Click on a payment to see the refund request details: * Customer's reason for the refund * Requested amount * Request date * Payment details (amount, date, product) *** ## Step 2: Process Refunds ### Approve or Reject For each refund request, you can: * **Approve**: The refund is processed and the customer is notified * **Reject**: The request is declined with an optional note to the customer Refund approval dialog ### What Happens After Approval 1. The refund amount is deducted from your balance 2. The customer receives a refund confirmation email 3. Funds are returned to the customer's original payment method 4. The payment status updates to reflect the refund Refunds are permanent. Once approved, they cannot be reversed. Review each request carefully before approving. *** ## Step 3: Track Refund Status Refund tickets go through these statuses: | Status | Meaning | | ------------ | ---------------------------------------------------- | | `pending` | Customer submitted request, awaiting your review | | `processing` | Refund approved, being processed by payment provider | | `succeeded` | Refund completed, funds returned to customer | | `rejected` | Request rejected by you | | `failed` | Refund failed during processing (rare) | You can filter and track all refunds in the **Payments** section of your Dashboard. *** ## Refund Policies ### Setting Expectations We recommend establishing clear refund policies and communicating them to customers: | Policy Element | Recommendation | | ---------------- | ------------------------------------------ | | **Time window** | 7–30 days after purchase | | **Conditions** | Product not as described, technical issues | | **Exclusions** | Consumed services, customized products | | **Process time** | 5–10 business days for funds to appear | Display your refund policy clearly on your checkout page and product pages. Clear policies reduce disputes and build customer trust. *** ## Subscription Refunds ### When a Subscriber Requests a Refund For subscription products, consider these options: 1. **Refund + Cancel**: Refund the current period and cancel the subscription 2. **Refund only**: Refund the current period but keep the subscription active 3. **Cancel only**: Cancel the subscription without a refund (customer keeps access until period ends) ### Recommended Approach For most cases, **refund + cancel** is the cleanest approach. The customer gets their money back, and the subscription is terminated. *** ## Business Scenarios ### Scenario 1: Digital Product Refund **Customer says:** "The course didn't cover what was advertised." **Recommended action:** 1. Review the refund request in Dashboard 2. If within your refund window (e.g., 14 days), approve the full refund 3. Customer receives confirmation and funds are returned ### Scenario 2: Subscription First-Month Refund **Customer says:** "I didn't realize I'd be charged after the trial." **Recommended action:** 1. Approve the refund for the first payment 2. Cancel the subscription 3. Consider improving trial-to-paid communication ### Scenario 3: Partial Refund for Service Issue **Customer says:** "The service was down for a week." **Recommended action:** 1. Calculate the proportional refund (e.g., 25% for 1 week of a month) 2. Issue a partial refund 3. Optionally extend the current period as a goodwill gesture *** ## Going Further: Code Integration ### Webhooks for Refunds If you use webhooks, you can automate post-refund actions: | Event | Automated Action | | ------------------ | ------------------------------------------ | | `refund.succeeded` | Revoke product access, update database | | Refund approved | Send internal notification to support team | ### API Refunds For programmatic refund processing, see the [API Reference — Refunds](/api-reference/endpoints/refunds). *** ## Best Practices 1. **Respond quickly**: Review refund requests within 24–48 hours 2. **Be fair**: When in doubt, lean toward the customer's side 3. **Keep records**: Document the reason for each refund decision 4. **Learn from patterns**: If many refunds cite the same reason, fix the underlying issue 5. **Communicate clearly**: Always explain refund decisions to customers *** ## Checklist * [ ] Refund policy established and communicated * [ ] Customer Portal enabled for self-service refund requests * [ ] Refund review process in place for your team * [ ] Webhook events configured for refund notifications (if using code integration) * [ ] Refund policy displayed on checkout/product pages *** ## Next Steps Automate post-refund actions Set up products with clear refund expectations # Run SaaS Subscriptions Source: https://docs.waffo.ai/guides/subscriptions Set up recurring billing with monthly/yearly plans, trials, and cancellation handling ## Who Is This For? This guide is for merchants who sell subscription-based services or products with recurring billing. Common examples include: | Business Type | Examples | | ----------------- | ---------------------------------------------------- | | SaaS Products | Project management tools, CRM, analytics platforms | | Content Platforms | Online learning, premium newsletters, media access | | Digital Services | Cloud hosting, API access, design tools | | Membership | Community access, premium support, exclusive content | *** ## Subscription Lifecycle Before setting up, it helps to understand how subscriptions flow: ``` Trial → Active → (Renew each period) → Canceled → Expired ↑ ↓ └───── Reactivate ────┘ ``` | Status | What It Means | | ----------- | --------------------------------------------------------- | | `trialing` | Customer is in a free trial period | | `active` | Paying customer, subscription is active | | `past_due` | Payment failed, Waffo is retrying automatically | | `canceling` | Customer canceled, but still has access until period ends | | `canceled` | Subscription has fully ended | *** ## Step 1: Create Subscription Products You'll typically create multiple products for different pricing tiers (e.g., Free, Pro, Enterprise). In your Dashboard, navigate to **Products** from the sidebar. Products list page Click **Create Product** and select **Subscription** as the product type. Create subscription product form * **Name**: Your plan name (e.g., "Basic Plan", "Pro Plan") * **Billing Period**: Choose monthly, quarterly, or yearly * **Price**: Set the recurring price * **Description**: What's included in this plan Create separate products for each billing period. For example, create both a monthly and yearly version of the same plan. Click **Save**. The product starts in test mode. Activate it when ready. *** ## Step 2: Organize Plans with Product Groups If you have multiple tiers (Free, Pro, Enterprise), use **Product Groups** to organize them. Go to **Products** and create a new product group (e.g., "Pricing Plans"). Select the subscription products that belong together. This helps manage pricing tiers as a unit. * **Shared Trial**: If enabled, a customer who used a trial on one plan can't get another trial on a different plan in the same group. Product groups are optional but recommended if you offer multiple pricing tiers. They prevent trial abuse and simplify management. *** ## Step 3: Share and Start Selling Just like one-time products, subscription products get a checkout link. ### Share the Checkout Link Copy the checkout link from the product detail page: ``` https://checkout.waffo.ai/your-store/my-product ``` ### Build a Pricing Page Create a pricing page on your website that links to each plan's checkout URL: > Prices below are examples only. Set them based on your actual business. | Plan | Price | Checkout Link | | ---------- | ---------- | ------------------------------------------------------ | | Basic | \$9/month | `https://checkout.waffo.ai/your-store/basic-plan` | | Pro | \$29/month | `https://checkout.waffo.ai/your-store/pro-plan` | | Enterprise | \$99/month | `https://checkout.waffo.ai/your-store/enterprise-plan` | *** ## Step 4: Monitor Subscriptions ### Subscriptions Dashboard Navigate to **Subscriptions** in the sidebar to see all active, trialing, and canceled subscriptions. Subscriptions list page Each subscription shows: * Customer email * Current plan * Status (active, trialing, past\_due, etc.) * Current period start and end dates * Next billing date ### Subscription Detail Click on any subscription to view details: * Payment history * Plan changes * Cancellation details (if applicable) *** ## Step 5: Handle Cancellations When a customer cancels, the subscription enters a **canceling** state. The customer keeps access until the current billing period ends, then it becomes **canceled**. ### What Happens Automatically * Customer receives a cancellation confirmation email * You receive a notification * Access continues until the period ends * No further charges are made ### View Cancellations in Dashboard Filter subscriptions by **Canceling** or **Canceled** status to see who has left. *** ## Business Scenarios The following are example scenarios. Adjust them to match your actual business. ### Scenario 1: SaaS with Monthly and Yearly Plans 1. **Create products**: Create both monthly and yearly versions of the same plan 2. **Group them**: Create a product group to link them together 3. **Pricing page**: Link each option on your website 4. **Monitor**: Track MRR and churn in the Analytics page ### Scenario 2: Content Platform with Free Trial 1. **Create product**: Set up your subscription plan and price 2. **Enable trial**: Set a 14-day free trial period 3. **Share link**: Customer signs up and starts trial immediately 4. **After trial**: Automatically converts to paid subscription, or expires if they don't add payment ### Scenario 3: API Service with Tiered Plans 1. **Create products**: Create a subscription product for each tier with different API call quotas and prices 2. **Group them**: Create a product group with shared trial 3. **Integrate webhooks**: Grant appropriate API limits based on which plan the customer subscribes to *** ## Going Further: Code Integration ### When You Need Code * **Access control**: Check subscription status in your app to gate features * **Automated provisioning**: Use webhooks to automatically create accounts or adjust limits * **Custom cancellation flows**: Build in-app cancellation with feedback collection ### Key Webhook Events | Event | When It Fires | | ------------------------ | --------------------------- | | `subscription.activated` | New subscription started | | `subscription.updated` | Plan changed or renewed | | `subscription.canceled` | Subscription fully ended | | `order.completed` | Recurring payment processed | | `subscription.past_due` | Payment attempt failed | For webhook setup, see the [Webhooks guide](/guides/webhooks). For API details, see the [API Reference](/api-reference/introduction). *** ## Testing Toggle to test mode in your Dashboard. Use `4576 7500 0000 0110` to simulate a successful subscription. Verify the subscription appears in your Subscriptions page with the correct plan and status. Cancel the test subscription and verify it transitions to canceling → canceled. *** ## Launch Checklist * [ ] Subscription products created for each plan/tier * [ ] Product group set up (if multiple tiers) * [ ] Pricing page links to correct checkout URLs * [ ] Test subscription flow works end-to-end * [ ] Cancellation flow tested * [ ] Notification emails look correct * [ ] Products published to production *** ## Next Steps Automate access control with subscription event notifications Combine subscriptions with metered usage # Set Up Webhooks Source: https://docs.waffo.ai/guides/webhooks Get notified when payments succeed, subscriptions renew, or refunds are processed ## What Are Webhooks? Webhooks are automatic notifications that Waffo Pancake sends to your server when something happens — a payment succeeds, a subscription renews, a refund is processed, etc. **Why you need them:** * **Automate delivery**: Send download links or grant access immediately after payment * **Keep your system in sync**: Update your database when subscription status changes * **React to events**: Handle failed payments, cancellations, and refunds in real time *** ## Step 1: Add a Webhook in the Dashboard In your Dashboard, navigate to **Settings → Webhooks**. Webhooks settings page with Test Mode and Live Mode sections Select how the payload should be shaped for the destination — Raw JSON for your own backend, a chat-platform format (Slack / Discord / Feishu / Telegram) so messages render natively in that tool, or **OpenClaw / Hermes** (the self-hosted Pancake plugin) for custom routing and downstream logic. See [Choose a payload format](#choose-a-payload-format) below. Add Webhook modal with the Format dropdown open showing Raw, Feishu, Slack, Discord, Telegram, OpenClaw/Hermes Enter the URL where Waffo Pancake should send event notifications. * **Test mode URL**: Your development/staging server (e.g., `https://staging.yoursite.com/webhooks/waffo`) * **Production URL**: Your production server (e.g., `https://yoursite.com/webhooks/waffo`) You can set different URLs for test and production environments. This keeps test events separate from real ones. If you picked **Telegram** in the previous step, you'll fill in a **Bot Token** and a **Chat ID** instead of a URL. See [Telegram](#telegram) below for how to obtain them. All events are subscribed by default. Untick the ones you don't want. Click **Save Webhook**. You can configure **up to 10 webhooks per environment**, so you can fan out the same events to multiple destinations (your backend, a Slack channel, an internal dashboard) without writing your own relay. The same URL cannot be added twice in the same environment. **Production note**: Live Mode webhooks only deliver after your store passes review. While the store is still under review, use Test Mode webhooks against staging code paths. *** ## Choose a payload format Waffo can deliver each event in the format your destination expects, so you don't have to write a translator. Pick the format when you add the webhook; you can re-add a webhook with a different format if you change your mind. | Format | Use when… | Endpoint | | --------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | **Raw** (default) | You're calling your own backend and want the canonical, signed JSON payload. | Any HTTPS URL | | **Feishu / Lark** | You want events posted into a Feishu group via a custom bot. | `https://open.feishu.cn/open-apis/bot/v2/hook/` | | **Slack** | You want events posted into a Slack channel via an Incoming Webhook. | `https://hooks.slack.com/services/T.../B.../` | | **Discord** | You want events posted into a Discord channel via a channel webhook. | `https://discord.com/api/webhooks//` | | **Telegram** | You want events posted into a Telegram chat or group via a bot. | Bot Token + Chat ID (URL is built for you) | | **OpenClaw / Hermes** | You're routing events through the self-hosted **Pancake plugin** for custom downstream logic. | `https://relay.waffo.ai/webhook/` | Only **Raw** carries an RSA-SHA256 signature. The chat-platform formats authenticate via the URL-embedded token issued by the destination platform, so signature verification is not used for them. ### Raw 1. In your backend, expose an HTTPS endpoint that accepts `POST application/json`. 2. Paste the URL into the **Endpoint URL** field. 3. On the same **Settings → Webhooks** page, copy the **Webhook Public Key** for the environment you're integrating (Test or Production) — you'll use it to verify deliveries (see [API Reference — Webhooks](/api-reference/webhooks)). The key is platform-level, shared across all stores; it doesn't depend on the webhook you just added, and it stays the same whenever you add, edit, or delete webhook URLs. ### Feishu / Lark 1. In your Feishu group, add a **Custom Bot** and copy its webhook URL. 2. The URL looks like `https://open.feishu.cn/open-apis/bot/v2/hook/` (or `open.larksuite.com/...` outside China — both work). 3. Paste it into the **Endpoint URL** field. ### Slack 1. In your Slack workspace, install an **Incoming Webhook** for the destination channel. 2. Copy the webhook URL — it looks like `https://hooks.slack.com/services/T.../B.../`. 3. Paste it into the **Endpoint URL** field. ### Discord 1. In the destination channel, open **Edit Channel → Integrations → Webhooks → New Webhook** and copy the URL. 2. Format: `https://discord.com/api/webhooks//`. 3. Paste it into the **Endpoint URL** field. ### Telegram Telegram needs both a **Bot Token** and a **Chat ID** — Waffo builds the Telegram `sendMessage` URL for you. 1. Talk to [@BotFather](https://t.me/botfather) and create (or pick) a bot. Copy its token, e.g. `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`. 2. Add the bot to the destination chat or channel and grant it permission to send messages. 3. Get the chat ID. The easiest way: send a message in the chat, then visit `https://api.telegram.org/bot/getUpdates` and copy the `chat.id` from the response. 4. In the **Add Webhook** modal, choose **Telegram**, paste the **Bot Token** and **Chat ID**, and save. Behind the scenes Waffo posts to `https://api.telegram.org/bot/sendMessage` with `chat_id` set to the value you provided. ### OpenClaw / Hermes (Pancake plugin) The Pancake plugin is Waffo's self-hosted relay. It receives events from Waffo, runs your custom logic (templating, rate-limiting, multi-fan-out), and forwards messages to wherever you want. **OpenClaw** and **Hermes** are two ready-to-run flavors of the plugin; pick whichever matches your stack. 1. Install the plugin with the matching setup command: ```bash theme={"system"} # OpenClaw npx -p @waffo/pancake-plugin openclaw-setup # Hermes npx -p @waffo/pancake-plugin hermes-setup ``` Full setup guide: [github.com/waffo-com/waffo-pancake-plugin](https://github.com/waffo-com/waffo-pancake-plugin). 2. After install, the plugin gives you a relay URL of the form `https://relay.waffo.ai/webhook/`. 3. In the **Add Webhook** modal, choose **OpenClaw / Hermes**, paste the relay URL, and save. *** ## Step 2: Understand Event Types Waffo Pancake sends these webhook events: ### Order Events | Event | When It Fires | Common Action | | ----------------- | ---------------------------- | ----------------------------- | | `order.completed` | Order completed successfully | Deliver product, grant access | ### Subscription Events | Event | When It Fires | Common Action | | -------------------------------- | ------------------------------ | --------------------------------- | | `subscription.activated` | Subscription activated | Create account, set limits | | `subscription.payment_succeeded` | Subscription payment succeeded | Extend access period | | `subscription.updated` | Subscription details changed | Update access level | | `subscription.canceling` | Cancellation requested | Notify team, send retention offer | | `subscription.uncanceled` | Subscription reactivated | Restore full access | | `subscription.canceled` | Subscription fully ended | Revoke access | | `subscription.past_due` | Payment failed | Notify customer, retry | ### Refund Events | Event | When It Fires | Common Action | | ------------------ | ------------------------ | ----------------------------- | | `refund.succeeded` | Refund processed | Revoke access, update records | | `refund.failed` | Refund processing failed | Notify merchant, investigate | *** ## Step 3: Set Up Email Notifications In addition to webhook events, you can configure email notifications for both you and your customers. ### Merchant Notifications Go to **Settings → Notifications** to choose which events trigger email alerts to you: * New orders * New subscriptions * Failed payments * Refund requests Notification settings ### Customer Notifications Customers automatically receive emails for: * Order confirmation * Subscription confirmation * Subscription renewal * Subscription cancellation * Payment failure You can toggle these in the notification settings. *** ## How Webhooks Work ``` Event occurs (e.g., payment succeeds) ↓ Waffo Pancake sends POST request to your webhook URL ↓ Your server receives the event ↓ Your server processes it (deliver product, update database, etc.) ↓ Your server responds with 200 OK ``` ### Retry Policy If your server doesn't respond with a 2xx status code, Waffo Pancake retries: | Attempt | Delay | | --------- | ---------- | | 1st retry | 5 minutes | | 2nd retry | 30 minutes | | 3rd retry | 2 hours | | 4th retry | 8 hours | | 5th retry | 24 hours | After 5 failed retries, the event is marked as failed. *** ## For Developers: Code Integration If you're integrating webhooks with your server, here's how the flow works: ### 1. Create a Webhook Endpoint Your server needs a POST endpoint to receive events. The specific implementation depends on your tech stack. ### 2. Verify the Signature Every webhook request includes a signature header for security. Verify it to ensure the request came from Waffo Pancake, not a malicious third party. ### 3. Process Events Parse the event type and data, then take the appropriate action (deliver product, update subscription status, etc.). ### 4. Respond Quickly Return a 200 status code as fast as possible. If you need to do heavy processing, do it asynchronously after responding. For detailed code examples and signature verification, see the [API Reference — Webhooks](/api-reference/webhooks). *** ## Testing Webhooks In Dashboard → Settings → Webhooks, enter your test server URL. Use test mode to create a purchase. This triggers webhook events to your test URL. Check your server logs to confirm the webhook was received and processed correctly. *** ## Local development with ngrok When you're building your webhook handler locally, Pancake's servers can't reach `http://localhost:3000` directly. You need a **public HTTPS tunnel** that forwards inbound requests to your local port. We recommend [ngrok](https://ngrok.com) — it's free for development and preserves all custom headers (including `X-Waffo-Signature`). ### Why a tunnel is required Webhook delivery is **inbound to your server**: Pancake POSTs to whatever URL you configure. A localhost URL is only reachable from your own machine, so without a tunnel the requests never arrive. The tunnel gives your local server a public HTTPS hostname for the duration of your dev session. ### Install and start ngrok * **macOS**: `brew install ngrok` * **Windows / Linux**: download from [ngrok.com/download](https://ngrok.com/download) Sign up at [ngrok.com](https://ngrok.com), grab your authtoken from the dashboard, then run: ```bash theme={"system"} ngrok config add-authtoken ``` Point ngrok at your local server's port. If your webhook handler runs on `localhost:3000`: ```bash theme={"system"} ngrok http 3000 ``` ngrok prints a forwarding URL like: ``` Forwarding https://abc-123-456-789.ngrok-free.app -> http://localhost:3000 ``` Copy the `https://...ngrok-free.app` URL — that's your public webhook endpoint. ### Wire it into Pancake In the Merchant Dashboard, open your store and go to **Settings → Webhooks**. Paste the ngrok URL (with your handler path appended, e.g. `https://abc-123.ngrok-free.app/api/webhooks/waffo`) into **Test Webhook URL** and select the events you want to receive. Save. Only configure Test environment with the ngrok URL. Never put a temporary tunnel URL into the Production webhook — production traffic must hit a stable, deployed endpoint. Either click **Send test event** in the Dashboard, or perform a real action in test mode (e.g., create a checkout and complete it with a [test card](/api-reference/webhooks#testing)). Open ngrok's local web inspector at [http://127.0.0.1:4040](http://127.0.0.1:4040) — it shows every request that came through, including headers and body. Confirm the `X-Waffo-Signature` header is present. Check your local server logs. You should see the webhook POST landing and `verifyWebhook()` succeeding. If verification fails, see the pitfalls below. ### Common pitfalls | Issue | Why it happens | Fix | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **`localtunnel` always returns 401 on signature verification** | localtunnel strips custom HTTP headers, so `X-Waffo-Signature` never reaches your handler | Use ngrok or cloudflared instead — both preserve all headers | | **The ngrok URL changed after restart** | The free tier issues a new random subdomain on every `ngrok http` run | Update the Test Webhook URL in the Dashboard each time. For frequent dev, ngrok's paid tier offers reserved subdomains | | **Events stop arriving after a while** | Free ngrok sessions expire after a few hours | Restart `ngrok http 3000` and re-paste the new URL | | **Pancake retried the event multiple times** | Your handler returned a non-2xx response and the delivery was retried with exponential backoff | Fix the handler, then either wait for the next retry or use **Resend** in the Dashboard's webhook delivery log | ### Cloudflared as an alternative If you prefer a no-account option, [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) works similarly: ```bash theme={"system"} cloudflared tunnel --url http://localhost:3000 ``` It preserves headers, requires no signup for ad-hoc tunnels, and prints a `*.trycloudflare.com` URL you can paste into the Dashboard. The trade-off is no built-in request inspector UI — you'll rely on your own server logs. ### Quick verification checklist * [ ] Local server is running and listening on the expected port * [ ] `ngrok http ` is running, forwarding URL captured * [ ] Test Webhook URL in Dashboard matches the **current** ngrok URL (with handler path) * [ ] Inbound request visible at [http://127.0.0.1:4040](http://127.0.0.1:4040) with `X-Waffo-Signature` header * [ ] Local logs show `verifyWebhook` returning success and your event handler running *** ## Best Practices 1. **Respond quickly**: Return 200 immediately, process asynchronously 2. **Handle duplicates**: Events may be sent more than once — make your processing idempotent 3. **Verify signatures**: Always verify the webhook signature before processing 4. **Log everything**: Keep logs of received webhooks for debugging 5. **Monitor failures**: Check your Dashboard for failed webhook deliveries *** ## Checklist * [ ] Webhook URLs configured for test and production * [ ] Email notifications configured for your team * [ ] Customer notification preferences set * [ ] Test webhook received and processed successfully * [ ] Signature verification implemented (if using code integration) *** ## Next Steps Handle refund requests and track refund status Detailed webhook payload format and code examples # Introduction Source: https://docs.waffo.ai/index Accept payments globally — we handle taxes, compliance, and payouts so you don't have to. ## Turn Your Software into a Business You built something people want. Now you need to get paid — globally, compliantly, without the headache. **Waffo Pancake handles everything between your product and your bank account:** payments, taxes, compliance, and payouts worldwide. Accept Visa, Mastercard, Apple Pay & Google Pay for faster checkout. Get paid directly to your bank account. We're your Merchant of Record. *** ## What's Blocking Your Revenue? You want to ship, not fight compliance. Here's what we solve for you: Traditional MoR services require you to register a legal entity first — that takes weeks and thousands of dollars. **Waffo Pancake's Solution:** We act as your Merchant of Record. You get paid directly to your bank account without needing an LLC. Subscription management, tax calculation, dunning emails, failed payment retries... that's months of engineering work. **Waffo Pancake's Solution:** Complete infrastructure out of the box — subscriptions, one-time payments, all handled. The US has 10,000+ tax jurisdictions, the EU has 27 countries each with VAT rules, plus constantly changing regulations... **Waffo Pancake's Solution:** We're the legal seller. Tax calculation, collection, and reporting are handled automatically. Complex checkout forms, no Apple Pay/Google Pay, no multi-language support — every step loses customers. **Waffo Pancake's Solution:** Optimized two-step checkout flow, Apple Pay & Google Pay support, built-in 7 languages. *** ## How Waffo Pancake Helps You Taxes, compliance, risk — all handled by us. You focus on your product. Visa, Mastercard, Apple Pay, and Google Pay. One-time or subscription — your business model, your choice. Weekly, monthly, quarterly, or annual billing. Two-step checkout flow, 7 languages supported, mobile-first design. Revenue, customers, subscription metrics — no SQL needed, everything at a glance. Refunds, customer portal, subscription management — all in one dashboard. *** ## Who Is This For? No company? No problem. Get paid directly to your bank account. We handle all compliance. Subscriptions, trials, seat-based billing, usage metering — complete solution designed for SaaS. AI-friendly integration via SDK, MCP Skill, and CLI — ship payments with natural language prompts. *** ## Start Getting Paid in 5 Minutes Sign up at [Merchant Dashboard](https://pancake.waffo.ai/merchant/auth/signin). No credit card required. Create a product and set pricing. Done in 2 minutes. Get your checkout link and add it to your website. Funds go directly to your bank account. Set up your first payment in 5 minutes. Browse everything Waffo Pancake offers. Safely test the complete flow before going live. *** ## Built for Developers Install the official TypeScript SDK and start accepting payments in a few lines: ```bash theme={"system"} npm install @waffo/pancake-ts ``` ```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!, }); // Create a checkout session — redirect consumer to checkoutUrl const session = await client.checkout.createSession({ storeId: "store_xxx", productId: "prod_xxx", productType: "onetime", currency: "USD", }); // => session.checkoutUrl ``` Or call the REST API directly: ```bash theme={"system"} curl -X POST https://api.waffo.ai/v1/actions/checkout/create-session \ -H "Content-Type: application/json" \ -H "X-Store-Slug: your-store-slug" \ -H "X-Environment: test" \ -d '{ "productId": "your-product-uuid", "productType": "onetime", "currency": "USD" }' ``` Official SDK with built-in signing and webhook verification. REST + GraphQL API with full endpoint documentation. Real-time event notifications. Seamless backend integration. *** ## Start Your Global Revenue Journey Don't let complex compliance slow down your product momentum. Start accepting payments in minutes. Built for developers. # AI Integration Source: https://docs.waffo.ai/integrate/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 → Merchant → API & Development (use this as `WAFFO_MERCHANT_ID`, not `storeId`) 2. Create an API Key from Dashboard → Merchant → 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. ``` Open the official skill file from the AI Integration page to view, copy, or download the exact `SKILL.md` used by the team. *** 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 Explain what you sell, how customers are charged, and which parts are fixed-price versus usage-based. Have the agent map your offers into one-time products, subscription products, product groups, and optional dynamic pricing flows. Confirm naming, billing periods, tax categories, and whether any add-ons should remain one-time charges. Use the output to create products in the Dashboard or to generate SDK/API integration code. ### 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 | 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. ### 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= WAFFO_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 | **`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. ```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` | Integration (集成) page → top section, with copy button | | `Store ID` | Settings (设置) → Store Profile (店铺资料) | | `Product ID` | Products (产品) → click a product → shown in URL and detail page | | `API Key` | Integration (集成) → 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 })` # Migrate to Waffo Pancake Source: https://docs.waffo.ai/integrate/migrate Move your products, prices, webhooks, and branding to Waffo Pancake with the pancake-migrate CLI Already running on Stripe or Creem? The [`@waffo/pancake-migrate`](https://www.npmjs.com/package/@waffo/pancake-migrate) CLI copies your products, prices, webhooks, and branding into a Waffo Pancake store — no manual re-entry. Migration moves your **catalog and settings**, not live billing. Active subscriptions, customers, and payment methods stay where they are — see [What does *not* migrate](#what-does-not-migrate) for why and what to do. On a different platform? We currently support migrating from **Stripe** and **Creem**, with more on the way. [Contact us](mailto:support@waffo.ai) and let us know what you're moving from — we'll prioritize it. ## How concepts map Before you run anything, here's how your existing setup lines up with Waffo Pancake. The CLI handles this mapping for you — this table is just so you know what to expect. | Stripe / Creem | Waffo Pancake | Notes | | ------------------ | --------------------------- | ----------------------------------------------------------------------------------------------------------- | | Product + Price | One product (with `prices`) | Pancake folds price into the product. Stripe multi-currency prices are preserved; Creem is single-currency. | | Recurring interval | `billingPeriod` | `week`→`weekly`, `month`→`monthly`, every 3 months→`quarterly`, `year`→`yearly`. | | Webhook endpoint | Store webhook | Stripe endpoints migrate with URL + event-type mapping. (Creem webhooks are not read by the CLI.) | | Account branding | Store branding | Stripe support email + website migrate. | | Tax behavior | `taxCategory` | Creem categories map automatically; for Stripe you pass one default via `--tax-category`. | | Test / live keys | `test` / `production` | Auto-detected from your key prefix (`sk_test_`→test, `sk_live_`→production). | ## Before you start You need three things: 1. **Node.js 18+** 2. **A Waffo Pancake API key** — your Merchant ID (`MER_...`) and RSA private key. Create one in [Dashboard → API & Development](https://pancake.waffo.ai/merchant/dashboard/integration). See [the SDK guide](/integrate/skill) for key handling. 3. **Your source platform key** — a Stripe Secret Key (`sk_test_...` / `sk_live_...`) or a Creem API Key (`creem_test_...` / `creem_...`). The migration **creates** products in the target environment that your key implies (test vs. live). Always do a `--dry-run` first to preview exactly what will be created. ## Quickest path: interactive mode ```bash theme={"system"} npx @waffo/pancake-migrate ``` It walks you through it step by step: Stripe or Creem. Pick an existing store, or create a new one on the spot. See exactly what will be migrated before anything is created. Products, prices, images, webhooks, and branding are created in your store. ## Scripted migration If you'd rather pass everything as flags (for CI, or to repeat the run), use the per-source subcommands. ### From Stripe ```bash theme={"system"} # Full migration (products + webhooks + branding) npx @waffo/pancake-migrate stripe \ --stripe-key sk_test_xxx \ --merchant-id MER_xxx \ --private-key ./private.pem \ --store-id STO_xxx \ --tax-category saas ``` Preview first with `--dry-run`: ```bash theme={"system"} npx @waffo/pancake-migrate stripe \ --stripe-key sk_test_xxx \ --merchant-id MER_xxx \ --private-key ./private.pem \ --store-id STO_xxx \ --tax-category saas \ --dry-run ``` Don't have a store yet? Pass `--store-id new` to create one during migration: ```bash theme={"system"} npx @waffo/pancake-migrate stripe \ --stripe-key sk_test_xxx \ --merchant-id MER_xxx \ --private-key ./private.pem \ --store-id new \ --tax-category saas ``` Skip steps you don't want with `--skip-products`, `--skip-webhooks`, or `--skip-branding`: ```bash theme={"system"} npx @waffo/pancake-migrate stripe ... --skip-webhooks --skip-branding ``` **Stripe flags** | Flag | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `--stripe-key ` | Stripe Secret Key (`sk_test_...` or `sk_live_...`) | | `--merchant-id ` | Pancake Merchant ID (`MER_...`) | | `--private-key ` | Path to your Pancake API private key file, or the raw key content | | `--store-id ` | Target Store ID (`STO_...`), or `new` to create one | | `--tax-category ` | Default tax category: `saas`, `digital_goods`, `software`, `ebook`, `online_course`, `consulting`, `professional_service` | | `--dry-run` | Preview without creating anything | | `--yes` | Skip the confirmation prompt | | `--skip-products` | Skip product migration | | `--skip-webhooks` | Skip webhook migration | | `--skip-branding` | Skip branding migration | ### From Creem ```bash theme={"system"} npx @waffo/pancake-migrate creem \ --api-key creem_test_xxx \ --merchant-id MER_xxx \ --private-key ./private.pem \ --store-id STO_xxx ``` Preview first: ```bash theme={"system"} npx @waffo/pancake-migrate creem \ --api-key creem_test_xxx \ --merchant-id MER_xxx \ --private-key ./private.pem \ --store-id STO_xxx \ --dry-run ``` **Creem flags** | Flag | Description | | ---------------------- | ----------------------------------------------------------------- | | `--api-key ` | Creem API Key (`creem_test_...` or `creem_...`) | | `--merchant-id ` | Pancake Merchant ID (`MER_...`) | | `--private-key ` | Path to your Pancake API private key file, or the raw key content | | `--store-id ` | Target Store ID (`STO_...`), or `new` to create one | | `--dry-run` | Preview without creating anything | | `--yes` | Skip the confirmation prompt | ## What gets migrated ### From Stripe | Data | Migrated | | ----------------- | ------------------------------------------------------------------------------------- | | Products + Prices | Yes — multi-currency, images, descriptions | | Billing periods | Yes — `week`→`weekly`, `month`→`monthly`, every 3 months→`quarterly`, `year`→`yearly` | | Webhook endpoints | Yes — URL + event-type mapping | | Store branding | Yes — support email + website | | Environment | Auto-detected — `sk_test_`→test, `sk_live_`→production | ### From Creem | Data | Migrated | | ----------------- | -------------------------------------------------------------------------------- | | Products + Prices | Yes — single-currency, images, descriptions | | Billing periods | Yes — every-month→`monthly`, every-three-months→`quarterly`, every-year→`yearly` | | Tax categories | Yes — `saas`→`saas`, `digital-goods-service`→`digital_goods`, `ebooks`→`ebook` |

What does *not* migrate

These are intentional — here's what to do about each: | Not migrated | Why | What to do | | --------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Active subscriptions | A subscription is a live billing authorization with the customer's bank; it can't be copied between processors | Existing subscribers re-subscribe through Waffo Pancake [checkout](/guides/checkout-session). Run the old and new processor in parallel during the transition. | | Customer data | Created automatically on first purchase | Nothing — customers are created on their first Pancake payment | | Payment methods | PCI compliance — card data never leaves the original processor | Customers re-enter payment details at checkout | | Coupons / discounts | Not supported by Pancake | — | | Checkout theme colors | Stripe doesn't expose these via API | Set them in [Dashboard → Settings → Checkout](https://pancake.waffo.ai/merchant/dashboard) | ## After migrating Open **Dashboard → Products** and confirm prices, billing periods, and images came across correctly. Products land in the environment your key implied. If you migrated into test, [publish](/integrate/skill) each product to go live. Point your app at Waffo Pancake [checkout sessions](/guides/checkout-session) and [webhooks](/guides/webhooks). Keep your old processor active while existing subscribers migrate at their own renewal. Stop new sign-ups on the old processor once Pancake checkout is live. Building the integration after migrating? The [Waffo Pancake Skill](/integrate/skill) gives AI coding agents the exact `@waffo/pancake-ts` workflow for checkout, webhooks, and subscriptions. # Next.js Integration Source: https://docs.waffo.ai/integrate/nextjs Build a complete payment flow with Next.js App Router Copy this prompt to your AI code editor (Cursor, Copilot, Claude Code, etc.) to set up the integration automatically: ```text theme={"system"} Integrate Waffo Pancake payments into my Next.js app using the official TypeScript SDK. npm install @waffo/pancake-ts Requirements: 1. Create /app/api/checkout/route.ts — use WaffoPancake client with client.checkout.createSession() 2. Create /app/api/webhooks/waffo/route.ts — use verifyWebhook() from SDK to verify x-waffo-signature 3. Add environment variables: WAFFO_MERCHANT_ID, WAFFO_PRIVATE_KEY, NEXT_PUBLIC_APP_URL Read https://waffo.mintlify.app/llms-full.txt for full API reference. ``` *** ## What You'll Build A complete checkout integration in Next.js including: * Server-side checkout session creation * Client-side redirect to hosted checkout * Webhook handling for payment confirmation * Protected routes based on payment status *** ## Prerequisites * Next.js 13+ with App Router * Waffo Pancake account with API keys * A product created in Dashboard *** ## Project Setup ### 1. Install Dependencies ```bash theme={"system"} npm install @waffo/pancake-ts ``` ### 2. Environment Variables ```bash theme={"system"} # .env.local WAFFO_MERCHANT_ID=your-merchant-id WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE..." NEXT_PUBLIC_APP_URL=http://localhost:3000 ``` The SDK accepts private keys in multiple formats: PEM, base64, or raw — it auto-normalizes at construction time. Literal `\n` in `.env` files works too. *** ## Create Checkout API Route Create an API route to generate checkout sessions using the SDK: ```typescript theme={"system"} // app/api/checkout/route.ts import { NextRequest, NextResponse } from "next/server"; import { WaffoPancake, CheckoutSessionProductType, WaffoPancakeError } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); export async function POST(req: NextRequest) { try { const { productId, email, metadata } = await req.json(); const session = await client.checkout.createSession({ storeId: "store_xxx", productId, productType: CheckoutSessionProductType.Onetime, currency: "USD", buyerEmail: email || undefined, metadata, successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/success`, }); return NextResponse.json({ checkoutUrl: session.checkoutUrl }); } catch (error) { if (error instanceof WaffoPancakeError) { return NextResponse.json({ error: error.errors[0]?.message }, { status: error.status }); } return NextResponse.json({ error: "Failed to create checkout" }, { status: 500 }); } } ``` The SDK automatically handles request signing and deterministic idempotency keys -- no manual header setup needed. *** ## Pricing Page Component Create a pricing page with checkout buttons: ```tsx theme={"system"} // app/pricing/page.tsx 'use client'; import { useState } from 'react'; const plans = [ { name: 'Starter', price: '$9', period: 'month', productId: 'prod_starter', features: ['5 projects', '10GB storage', 'Email support'], }, { name: 'Pro', price: '$29', period: 'month', productId: 'prod_pro', features: ['Unlimited projects', '100GB storage', 'Priority support', 'API access'], popular: true, }, { name: 'Enterprise', price: '$99', period: 'month', productId: 'prod_enterprise', features: ['Everything in Pro', 'Custom integrations', 'Dedicated support', 'SLA'], }, ]; export default function PricingPage() { const [loading, setLoading] = useState(null); async function handleCheckout(productId: string) { setLoading(productId); try { const response = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId }), }); const { checkoutUrl, error } = await response.json(); if (error) { alert(error); return; } // Redirect to Waffo Pancake checkout window.location.href = checkoutUrl; } catch (error) { alert('Something went wrong'); } finally { setLoading(null); } } return (

Choose Your Plan

{plans.map((plan) => (
{plan.popular && ( Most Popular )}

{plan.name}

{plan.price} /{plan.period}

    {plan.features.map((feature) => (
  • {feature}
  • ))}
))}
); } function CheckIcon({ className }: { className: string }) { return ( ); } ``` *** ## Webhook Handler Handle payment confirmations using the SDK's built-in `verifyWebhook()` -- it has embedded public keys for both test and production environments, so you don't need to manage webhook secrets: ```typescript theme={"system"} // app/api/webhooks/waffo/route.ts import { NextResponse } from "next/server"; import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts"; export async function POST(request: Request) { const body = await request.text(); const signature = request.headers.get("x-waffo-signature"); try { const event = verifyWebhook(body, signature); // Respond immediately, process asynchronously switch (event.eventType) { case WebhookEventType.OrderCompleted: console.log(`Order completed: ${event.data.orderId}`); // Update your database, grant access, etc. break; case WebhookEventType.SubscriptionActivated: console.log(`Subscription activated: ${event.data.buyerEmail}`); break; case WebhookEventType.SubscriptionCanceling: console.log(`Subscription canceling: ${event.data.orderId}`); break; case WebhookEventType.SubscriptionCanceled: console.log(`Subscription canceled: ${event.data.orderId}`); break; case WebhookEventType.RefundSucceeded: console.log(`Refund succeeded: ${event.data.amount} ${event.data.currency}`); break; } return NextResponse.json({ received: true }); } catch { return new Response("Invalid signature", { status: 401 }); } } ``` The SDK's `verifyWebhook()` uses embedded public keys -- no `WAFFO_WEBHOOK_SECRET` environment variable needed. It also includes replay protection by default. *** ## Success Page Show confirmation after successful payment: ```tsx theme={"system"} // app/success/page.tsx import { Suspense } from 'react'; export default function SuccessPage() { return ( Loading...}> ); } async function SuccessContent() { return (
); } ``` *** ## Protected Routes Middleware Protect routes based on subscription status: ```typescript theme={"system"} // middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // Get user session (implement your auth logic) const session = request.cookies.get('session'); // Protected routes const protectedPaths = ['/dashboard', '/settings', '/projects']; const isProtectedPath = protectedPaths.some(path => request.nextUrl.pathname.startsWith(path) ); if (isProtectedPath && !session) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*', '/settings/:path*', '/projects/:path*'], }; ``` *** ## Server Component: Check Subscription ```tsx theme={"system"} // app/dashboard/page.tsx import { redirect } from 'next/navigation'; import { getServerSession } from 'your-auth-library'; export default async function DashboardPage() { const session = await getServerSession(); if (!session) { redirect('/login'); } // Get user's subscription from database const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { plan: true, subscriptionActive: true, subscriptionEndsAt: true, }, }); if (!user?.subscriptionActive) { redirect('/pricing'); } return (

Welcome to your Dashboard

Your current plan: {user.plan}

{/* Dashboard content */}
); } ``` *** ## Customer Portal Link Let users manage their subscription: ```tsx theme={"system"} // components/ManageSubscription.tsx 'use client'; export function ManageSubscriptionButton({ email }: { email: string }) { const portalUrl = `https://checkout.waffo.ai/your-store/portal?email=${encodeURIComponent(email)}`; return ( Manage Subscription ); } ``` *** ## Complete File Structure ``` app/ ├── api/ │ ├── checkout/ │ │ └── route.ts # Create checkout sessions │ └── webhooks/ │ └── waffo/ │ └── route.ts # Handle webhooks ├── pricing/ │ └── page.tsx # Pricing page ├── success/ │ └── page.tsx # Success page ├── dashboard/ │ └── page.tsx # Protected dashboard └── layout.tsx middleware.ts # Route protection .env.local # Environment variables ``` *** ## Testing Checklist Before going live: * [ ] Test checkout flow with test card `4576 7500 0000 0110` * [ ] Verify webhooks are received (check Dashboard logs) * [ ] Test declined payment with `4576 7500 0000 0220` * [ ] Confirm success page displays correctly * [ ] Switch to live API keys * [ ] Update webhook URL to production endpoint *** ## Next Steps Deep dive into webhook handling Advanced subscription features # SDKs Source: https://docs.waffo.ai/integrate/sdks Client libraries and framework integrations ## SDKs & Libraries Waffo Pancake provides official SDKs for TypeScript and Next.js, plus direct API access. ### Choose Your Integration Level **Fastest** — Drop-in components, hooks, and server actions. Handles checkout UI, webhook routing, and buyer self-service out of the box. `npm install @waffo/pancake-nextjs` **Flexible** — Full API coverage with automatic signing, typed responses, and webhook verification. Use with any Node.js framework. `npm install @waffo/pancake-ts` **Full control** — REST endpoints for writes, GraphQL for reads. Implement signing and verification yourself. Any language. **Next.js projects**: Start with `@waffo/pancake-nextjs` — it wraps `@waffo/pancake-ts` and adds React components and server actions. You can always drop down to the TypeScript SDK or raw API calls for advanced use cases. *** ## TypeScript SDK Official TypeScript SDK on npm ### Installation ```bash theme={"system"} npm install @waffo/pancake-ts ``` ### Features * Zero runtime dependencies, ESM + CJS, Node >= 20 * Automatic request signing with deterministic idempotency keys * Full TypeScript type definitions (15 enums, 40+ interfaces) * Webhook verification with embedded public keys (test/prod) ### Quick Start ```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!, }); // Create a store const { store } = await client.stores.create({ name: "My Store" }); // Create a one-time product const { product } = await client.onetimeProducts.create({ storeId: store.id, name: "E-Book: TypeScript Handbook", prices: { USD: { amount: "29.00", taxIncluded: false, taxCategory: "digital_goods" }, }, }); // Create a checkout session const session = await client.checkout.createSession({ productId: product.id, currency: "USD", }); // => redirect consumer to session.checkoutUrl ``` ### Configuration | Parameter | Type | Required | Description | | ------------ | -------------- | -------- | ------------------------------------------------------- | | `merchantId` | `string` | Yes | Your Merchant ID | | `privateKey` | `string` | Yes | RSA private key (PEM, base64, or raw — auto-normalized) | | `baseUrl` | `string` | No | API base URL (default: production) | | `fetch` | `typeof fetch` | No | Custom fetch implementation | ### Available Resources | Namespace | Methods | Description | | ---------------------------------- | -------------------------------------------------- | -------------------------------------- | | `client.stores` | `create()` `update()` `delete()` | Store management | | `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time product CRUD | | `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription product CRUD | | `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups for shared trial | | `client.orders` | `cancelSubscription()` | Order management | | `client.checkout` | `createSession()` | Create checkout sessions | | `client.graphql` | `query()` | Typed GraphQL queries | | `client.auth` | `issueSessionToken()` | Issue buyer session token for checkout | | `client.webhooks` | `verify()` | Webhook signature verification | ### Checkout Modes The SDK supports two checkout modes based on whether you know the buyer's identity: | Mode | Method | Buyer Identity | Form | Use Case | | ----------------- | --------------------------------- | ----------------- | ---------- | ----------------------------- | | **Authenticated** | `checkout.authenticated.create()` | Merchant provides | Pre-filled | Sites with user accounts | | **Anonymous** | `checkout.anonymous.create()` | Not provided | Empty | Template stores, shared links | **Always use authenticated checkout when you know the buyer.** Authenticated checkout binds orders to the `buyerIdentity` you provide — a stable, merchant-controlled identifier. Without it: * **Trial abuse**: buyers can claim unlimited free trials by changing their email * **Order unlinking**: different emails are treated as different users * **No self-service**: buyers cannot manage orders in Customer Portal #### Authenticated Checkout (Recommended) ```typescript theme={"system"} const result = await client.checkout.authenticated.create({ productId: "PROD_xxx", currency: "USD", buyerIdentity: "userIdInYourSystem", }); // Redirect: res.redirect(result.checkoutUrl) or window.location.href = result.checkoutUrl ``` `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. With dynamic pricing and trial control: ```typescript theme={"system"} 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" }, withTrial: true, billingDetail: { country: "US", isBusiness: false }, successUrl: "https://example.com/thank-you", }); ``` #### Anonymous Checkout ```typescript theme={"system"} const result = await client.checkout.anonymous.create({ productId: "PROD_xxx", currency: "USD", }); // Also supports priceSnapshot and withTrial ``` ### Webhook Verification ```typescript theme={"system"} import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts"; // Express (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, ); res.status(200).send("OK"); switch (event.eventType) { case WebhookEventType.OrderCompleted: // handle order completion break; case WebhookEventType.SubscriptionActivated: // handle subscription activation 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); // handle event ... return new Response("OK"); } catch { return new Response("Invalid signature", { status: 401 }); } } ``` ### Error Handling ```typescript theme={"system"} 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" }, ...] } } ``` *** ## Next.js SDK Official Next.js SDK — components, hooks, and server actions ### Installation ```bash theme={"system"} npm install @waffo/pancake-nextjs ``` Requires `@waffo/pancake-ts` as a peer dependency. Works with Next.js 14+ App Router. ### Client Exports | Export | Type | Description | | ---------------------------- | ------------- | -------------------------------------------------------------- | | `CheckoutButton` | Component | Three-mode checkout button (link / anonymous / authenticated) | | `WaffoPancakeProvider` | Component | Buyer self-service context with automatic token lifecycle | | `useCheckout()` | Hook | Programmatic checkout control | | `useBuyer()` | Hook | Buyer self-service actions (cancel, refund, reactivate, query) | | `useBuyerOrders()` | Hook | Fetch buyer's orders | | `useBuyerPayments()` | Hook | Fetch buyer's payment records | | `useBuyerRefundTickets()` | Hook | Fetch buyer's refund tickets | | `useMerchantOrders()` | Hook | Merchant dashboard orders | | `useMerchantSales()` | Hook | Merchant sales overview | | `useMerchantSubscriptions()` | Hook | Merchant subscription overview | | `Webhook` | Route Handler | Auto-verify signatures and dispatch events | ### Server Exports (`@waffo/pancake-nextjs/server`) | Export | Description | | ----------------------------------- | -------------------------------------------- | | `createCheckoutAction(config)` | Server Action: create checkout sessions | | `createBuyerTokenAction(config)` | Server Action: issue buyer session tokens | | `createBuyerSessionAction(config)` | Server Action: buyer self-service operations | | `createMerchantQueryAction(config)` | Server Action: merchant GraphQL queries | ### Quick Example ```typescript theme={"system"} // app/lib/actions.ts "use server"; import { createCheckoutAction, createBuyerTokenAction } from "@waffo/pancake-nextjs/server"; const config = { merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }; export const checkoutAction = createCheckoutAction(config); export const issueBuyerToken = createBuyerTokenAction(config); ``` ```tsx theme={"system"} // app/components/BuyButton.tsx "use client"; import { CheckoutButton } from "@waffo/pancake-nextjs"; import { checkoutAction } from "@/app/lib/actions"; export function BuyButton({ productId }: { productId: string }) { return ( Subscribe Now ); } ``` ### Webhook Route Handler ```typescript theme={"system"} // app/api/webhooks/waffo/route.ts import { Webhook } from "@waffo/pancake-nextjs"; export const POST = Webhook({ onOrderCompleted: async (event) => { // Deliver digital goods }, onSubscriptionActivated: async (event) => { // Provision access }, onSubscriptionCanceled: async (event) => { // Revoke access }, }); ``` Full step-by-step Next.js integration guide. *** ## Direct API Access You can also use the REST and GraphQL API directly. API Key authentication is handled automatically by the SDK -- no manual header setup is needed. ```typescript theme={"system"} // Create a product const { product } = await client.onetimeProducts.create({ storeId: "your-store-uuid", name: "My Product", prices: { USD: { amount: "29.00", taxIncluded: false, taxCategory: "saas" }, }, }); // Query data via GraphQL const data = await client.graphql.query<{ stores: Array<{ id: string; name: string; status: string }> }>( `{ stores { id name status } }` ); ``` *** ## SDK Roadmap | Language | Status | | ------------------------ | ------------------------------------------------------------------------------------------ | | **Node.js / TypeScript** | Available — [`@waffo/pancake-ts`](https://www.npmjs.com/package/@waffo/pancake-ts) | | **Next.js** | Available — [`@waffo/pancake-nextjs`](https://www.npmjs.com/package/@waffo/pancake-nextjs) | | **Python** | Planned | | **Go** | Planned | | **PHP** | Planned | *** ## Framework Guides Build with Next.js and Server Actions. Create checkout flows programmatically. Implement recurring billing. Receive real-time event notifications. *** ## API Reference For complete endpoint documentation: Full REST and GraphQL API documentation. # Waffo Pancake Skill Source: https://docs.waffo.ai/integrate/skill Copy or download the exact Waffo Pancake skill file used for AI-assisted integration Portable, machine-readable guidance that gives AI coding agents the exact Waffo Pancake integration workflow. This is the official Waffo Pancake Skill file. The content stays aligned with the team-managed source skill and is distributed as a standard English `SKILL.md`. ## Available Files | File | Purpose | | --------------- | ------------------------------------------------------ | | `SKILL.md` | Complete Waffo Pancake integration guide for AI agents | | `llms-full.txt` | Full AI-readable documentation snapshot | ## SKILL.md: The Complete Guide This is the main skill file for AI-assisted Pancake integrations. It includes: * Merchant authentication and required environment variables * Store setup and product creation guidance * Product creation for one-time and subscription products * Checkout session creation * Webhook verification, event handling, and local `ngrok` testing * GraphQL queries, error handling, and integration rules ### Quick Reference Save the standard `SKILL.md` locally after copying or downloading it from this page. Inspect the complete standard `SKILL.md` source directly on this page before downloading it. ↓ View Full SKILL.md Below Use the complete skill file in your AI coding workflow. You can copy the full skill content or download it directly as a standard `SKILL.md` file. ## What's Inside * Platform overview and supported billing models * SDK-first setup flow and direct dashboard-to-checkout flow * Merchant authentication and environment setup * Product, checkout, and webhook implementation examples * Test cards, common gotchas, and critical integration rules ## Installation ```bash theme={"system"} mkdir -p ~/.waffo/skills ``` Then copy or download the standard `SKILL.md` file from this page. ## For AI Agents 1. Save `SKILL.md` locally so you can reference it without re-fetching every conversation. 2. Use the Skill first when the human asks about Pancake products, checkout, subscriptions, webhooks, or merchant setup. 3. Follow the product, checkout, and webhook flows exactly as written in the Skill file. ## Full Documentation For a full documentation snapshot beyond the skill file itself: ```bash theme={"system"} curl -s https://docs.waffo.ai/llms-full.txt ``` ## Full SKILL.md Content Use the built-in copy control on the code block to copy the full Skill. ````md theme={"system"} --- name: waffo-pancake description: "Step-by-step guide for integrating @waffo/pancake-ts into any TypeScript/Node.js project — covers store setup, products, checkout, webhooks, subscriptions, and GraphQL queries. Use when implementing payment processing, checkout flows, subscriptions, webhooks, product management, or any Waffo Pancake API integration." user-invokable: true args: "[scenario]" --- # Waffo Pancake SDK Integration You are integrating the `@waffo/pancake-ts` payment SDK into a TypeScript project. Follow these steps exactly. The SDK uses Merchant API Key authentication — all requests are signed automatically. **Trigger when:** code imports `@waffo/pancake-ts`, user asks about Waffo Pancake payments, checkout integration, webhook verification, or product/order management. **Do NOT trigger when:** general payment concepts, other payment SDKs (Stripe, PayPal), or frontend-only UI work unrelated to payment logic. --- ## When to Use This SDK Waffo Pancake is a merchant-of-record payment platform. Use it when your project needs: - **SaaS subscription billing** — monthly/yearly plans with upgrade/downgrade, cancellation, and renewal handling - **Digital product sales** — one-time purchases for e-books, templates, courses, software licenses - **Per-usage or per-download payments** — charge users per action like downloading a file or API call credits - **Hybrid models** — combine subscriptions with one-time purchases ### Example Use Cases | Project Type | Payment Model | Products to Create | |---|---|---| | AI Skills marketplace | Per-download + Pro subscription | 1 one-time product ($0.99/download) + 2 subscription products (monthly/yearly) | | Online course platform | One-time purchase per course | 1 one-time product per course ($29–$199) | | SaaS tool (Starter/Pro/Enterprise) | Subscription tiers | 3 subscription products with a product group for plan switching | | Design template shop | One-time per template | 1 one-time product per template | | API service with usage credits | Credit packs + subscription | 1 one-time product per credit pack + subscription for monthly quota | | Newsletter / community | Membership subscription | 1 monthly + 1 yearly subscription product | --- ## Installation ```bash npm install @waffo/pancake-ts ``` Zero dependencies. Works in Node.js 18+. Server-side only — never expose the private key to the browser. --- ## Getting Started You only need **two values** to start: ```bash WAFFO_MERCHANT_ID= WAFFO_PRIVATE_KEY= ``` These are provided when you sign up at Waffo Pancake Dashboard → API & Development. `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. ### Store Selection Rule When creating products, first determine whether the merchant already has one or more stores: - If the merchant has no store yet, create a store first. - If the merchant has exactly one store, use that store automatically. - If the merchant has multiple stores, ask which store the product should be created in before calling any product creation API. Do not guess the target store when multiple stores exist. ### Path A: Create Everything via SDK (Starting Fresh) ```typescript import { WaffoPancake } from "@waffo/pancake-ts"; const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); // 1. Create a store const { store } = await client.stores.create({ name: "My SaaS" }); console.log("Store ID:", store.id); // Save this // 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" } }, }); console.log("Monthly Product ID:", monthly.id); // Save this console.log("Yearly Product ID:", yearly.id); // Save this // 3. Create a checkout session const session = await client.checkout.createSession({ productId: monthly.id, productType: "subscription", currency: "USD", }); // Redirect customer to session.checkoutUrl ``` ### Path B: Use Existing Products from Dashboard If you've already created products 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 const client = new WaffoPancake({ merchantId: process.env.WAFFO_MERCHANT_ID!, privateKey: process.env.WAFFO_PRIVATE_KEY!, }); 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 ``` --- ## PEM Key Handling The private key is RSA PEM format. Multiple approaches for environment variables: **Option A: Escaped newlines** ``` WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----" ``` **Option B: Base64 encode the entire PEM (recommended for CI/CD)** ```bash # Encode cat private.pem | base64 | tr -d '\n' # Set env var WAFFO_PRIVATE_KEY_BASE64=LS0tLS1CRUdJTi... ``` ```typescript const privateKey = Buffer.from(process.env.WAFFO_PRIVATE_KEY_BASE64!, "base64").toString("utf-8"); const client = new WaffoPancake({ merchantId, privateKey }); ``` **Option C: File path (local development)** ```typescript import { readFileSync } from "fs"; const privateKey = readFileSync("./keys/private.pem", "utf-8"); const client = new WaffoPancake({ merchantId, privateKey }); ``` The SDK auto-normalizes all formats (PEM headers, raw base64, literal `\n`, Windows line endings). The constructor throws immediately if the key is invalid. --- ## Available Resources | Namespace | Methods | Description | |-----------|---------|-------------| | `client.auth` | `issueSessionToken()` | Issue buyer session token for checkout | | `client.stores` | `create()` `update()` `delete()` | Store management (webhook, notification, checkout settings) | | `client.onetimeProducts` | `create()` `update()` `publish()` `updateStatus()` | One-time product CRUD with multi-currency pricing | | `client.subscriptionProducts` | `create()` `update()` `publish()` `updateStatus()` | Subscription product CRUD with billing period | | `client.subscriptionProductGroups` | `create()` `update()` `delete()` `publish()` | Product groups for shared trial and plan switching | | `client.orders` | `cancelSubscription()` | Cancel subscription (pending→canceled, active→canceling) | | `client.checkout` | `createSession()` | Create checkout session, returns `checkoutUrl` | | `client.graphql` | `query()` | Typed GraphQL queries (read-only) | | `client.webhooks` | `verify()` | Webhook signature verification | --- ## Full API Reference ### Stores ```typescript // 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 Prices use the **display amount** for the selected currency. For USD, pass `"29.00"` instead of cents. ```typescript // Create 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 version; no-op if unchanged) 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 version to production (one-way) 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 // Create 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" }, }, metadata: { trialDays: 14 }, // optional trial }); // Update, publish, updateStatus — same pattern as one-time products ``` ### Subscription Product Groups Groups enable shared trials and plan switching between subscription products. ```typescript // Create group 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 a full replacement) await client.subscriptionProductGroups.update({ id: "group_id", productIds: ["monthly_id", "quarterly_id", "yearly_id"], }); // Publish to production (supports repeated UPSERT) await client.subscriptionProductGroups.publish({ id: "group_id" }); // Delete (physical delete, not soft) await client.subscriptionProductGroups.delete({ id: "group_id" }); ``` ### Checkout Sessions Creates a payment page. Redirect the customer to `checkoutUrl`. ```typescript 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" }, // dynamic pricing billingDetail: { country: "US", isBusiness: false }, expiresInSeconds: 3600, // default: 2700 (45 minutes) withTrial: true, // for subscriptions with trial darkMode: true, // true=dark / false=light / omit=store default }); // session.checkoutUrl — redirect customer here // session.sessionId — for tracking // session.expiresAt — ISO 8601 expiry ``` **Parameter priority:** | Parameter | Purpose | Notes | |-----------|---------|-------| | `priceSnapshot` | Override product price (dynamic pricing) | Highest priority — ignores product's set price | | `currency` | Specify checkout currency | Required | | `buyerEmail` | Pre-fill consumer email | Optional | | `billingDetail` | Pre-fill billing info | Optional | | `successUrl` | Redirect after payment | Overrides product-level successUrl | | `metadata` | Custom key-value pairs | Passed through to webhook `event.data` | | `withTrial` | Enable trial period | Overrides product-level trial settings | | `expiresInSeconds` | Session expiration | Default 2700 (45 min), max 7 days | | `darkMode` | Checkout dark mode | `true`=dark / `false`=light / omit=store default | ### Cancel Subscription ```typescript const { orderId, status } = await client.orders.cancelSubscription({ orderId: "order_id", }); // status: "canceled" (was pending) or "canceling" (was active, ends at period end) ``` ### GraphQL Queries Read-only queries. Return type is `{ data: T | null, errors?: [...] }` — access via `result.data`. ```typescript 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 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" }, }); ``` --- ## Webhook Verification Webhooks use RSA-SHA256 signatures. The SDK embeds public keys for both test and prod environments. **Critical:** Read the request body as **raw text**, not parsed JSON. Parsing first breaks signature verification. ### Next.js App Router ```typescript import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts"; export async function POST(request: Request) { const body = await request.text(); // MUST be raw text const sig = request.headers.get("x-waffo-signature")!; try { const event = verifyWebhook(body, sig); // Idempotent dedup — use event.id (delivery ID) if (await isDuplicate(event.id)) return new Response("OK"); await markProcessed(event.id); switch (event.eventType) { case WebhookEventType.OrderCompleted: // fulfill order break; case WebhookEventType.SubscriptionActivated: // activate access break; case WebhookEventType.SubscriptionCanceled: // revoke access at period end break; } return new Response("OK"); } catch { return new Response("Invalid signature", { status: 401 }); } } ``` ### Express ```typescript import express from "express"; import { verifyWebhook } from "@waffo/pancake-ts"; 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"); // handle event async... } catch { res.status(401).send("Invalid signature"); } }); ``` ### Hono ```typescript import { verifyWebhook } from "@waffo/pancake-ts"; app.post("/webhooks", async (c) => { const body = await c.req.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 // Specify environment explicitly verifyWebhook(body, sig, { environment: "prod" }); // Disable replay protection (not recommended for production) verifyWebhook(body, sig, { toleranceMs: 0 }); // Custom tolerance (default: 5 minutes) verifyWebhook(body, sig, { toleranceMs: 600000 }); ``` ### Webhook Event Types | Event | Trigger | |-------|---------| | `order.completed` | One-time payment succeeded | | `subscription.activated` | First subscription payment succeeded | | `subscription.payment_succeeded` | Renewal payment succeeded | | `subscription.canceling` | Buyer initiated cancel (active until period end) | | `subscription.uncanceled` | Buyer withdrew cancellation | | `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 | ### Webhook Event Shape ```typescript interface WebhookEvent { id: string; // Delivery ID (use for idempotent dedup) timestamp: string; // ISO 8601 UTC eventType: string; eventId: string; // Business event ID (payment/order ID) storeId: string; mode: "test" | "prod"; data: { orderId: string; buyerEmail: string; currency: string; amount: number; // display amount (for example 9.99 USD) taxAmount: number; productName: string; }; } ``` ### Configuring Webhook URLs via SDK 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 // 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"], 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"], testMode: false, }); // Update / remove await client.webhooks.update({ id: "WBH_xxx", events: ["order.completed"] }); await client.webhooks.remove({ id: "WBH_xxx" }); ``` To list webhooks, use the GraphQL `Store.storeWebhooks` field — it is the only query entry point. --- ## Common Gotchas | Gotcha | Why it breaks | Fix | |--------|---------------|-----| | Reading webhook body as JSON | `request.json()` re-serializes; signature fails | Use `request.text()` (App Router/Hono) or `express.raw()` (Express) | | Using localtunnel for webhooks | Strips custom HTTP headers; `X-Waffo-Signature` never arrives | Use `ngrok http 3000` | | Forgetting `.publish()` | Products created in test env by default; prod checkout sessions fail silently | Call `.publish()` before going live | | Accessing `result` instead of `result.data` in GraphQL | GraphQL returns `{ data: T, errors?: [...] }` | Destructure: `const stores = result.data?.stores ?? []` | | Using `$id: ID!` in GraphQL variables | Backend uses `String!` not `ID!`; wrong type silently returns null | Always declare ID variables as `$id: String!` | | `productIds` in group update is full replacement | Replaces entire list, does not append | Pass complete desired list every time | --- ## Error Handling ```typescript import { WaffoPancakeError } from "@waffo/pancake-ts"; try { await client.stores.create({ name: "" }); } catch (err) { if (err instanceof WaffoPancakeError) { console.log(err.status); // HTTP status code console.log(err.errors); // [{ message, layer }] console.log(err.errors[0].layer); // "store" | "product" | "order" | ... } } ``` Errors ordered by call stack depth: `errors[0]` = deepest layer (root cause), `errors[n]` = outermost. | Status | Cause | Fix | |--------|-------|-----| | 400 | Invalid request body | Check required fields and types | | 401 | Bad signature or expired token | Verify API key and private key match | | 403 | `prodEnabled=false` | Complete KYB review in dashboard | | 403 | Product not found or not accessible | Check the product belongs to the current merchant and environment | | 409 | Idempotency conflict or duplicate nickname | Wait and retry, or use unique nickname | | 429 | Rate limited | Back off and retry | --- ## Critical Rules ### ALWAYS DO - **Raw body for webhooks**: `express.raw()` or `request.text()`. Parsed JSON breaks signatures. - **New tab for checkout**: `window.open(url, "_blank", "noopener,noreferrer")`. Preserves merchant page state. - **Built-in verification**: `verifyWebhook()` has embedded public keys for test and prod. - **Use display amounts**: pass `"29.00"` for USD instead of cents-based integers. - **Env vars for secrets**: `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY` in `.env`. - **Separate API keys**: Different keys for test and production environments. - **Respond 200 immediately**: In webhook handlers, respond before async processing. - **Use `ngrok` for local tunneling**: It preserves custom HTTP headers. localtunnel strips them. ### NEVER DO - **Never** use `express.json()` on webhook routes. - **Never** use `window.location.href` for checkout redirect. - **Never** implement RSA-SHA256 signing manually — the SDK handles it. - **Never** hardcode private keys in source code. - **Never** use the same API key for test and production. - **Never** mix cents-based integers with display amounts in the same integration. --- ## Data Conventions | Type | Format | Example | |------|--------|---------| | Amounts | Display amount string | `"29.00"` for USD | | Currency | ISO 4217 | `USD`, `EUR`, `JPY` | | Timestamps | ISO 8601 UTC | `2026-01-23T00:00:00.000Z` | | IDs | `{PREFIX}_{base62}` | `STO_xxx`, `PROD_xxx`, `ORD_xxx` | | Checkout Session ID | `cs_` + UUID | `cs_550e8400-...` | --- ## Dashboard UI Glossary (EN → ZH → JA) The Dashboard supports English, Chinese, and Japanese. When referencing a Dashboard location, use this mapping. | English | 中文 | 日本語 | |---------|------|--------| | Home | 首页 | ホーム | | Products | 产品 | 商品 | | Customers | 客户 | 顧客 | | Analytics | 分析 | 分析 | | Payments | 付款 | 支払い | | Subscriptions | 订阅 | サブスクリプション | | Revenue | 收入 | 収益 | | Integration | 集成 | インテグレーション | | Settings | 设置 | 設定 | | Merchant ID | 商户 ID | マーチャントID | | Store ID | 店铺 ID | ストアID | | API Key | API 密钥 | APIキー | | Private Key | 私钥 | 秘密鍵 | | Test Mode | 测试模式 | テストモード | | Live Mode | 生产模式 | 本番モード | | One-time | 一次性 | 単発 | | Subscription | 订阅 | サブスクリプション | | Active | 生效中 | 有効 | | Canceled | 已取消 | キャンセル済み | | Completed | 已完成 | 完了 | **Where to find IDs:** - `WAFFO_MERCHANT_ID` → Dashboard → API & Development (集成) page, top section with copy button - `Store ID` → Dashboard → Settings (设置) → Store Profile (店铺资料) - `Product ID` → Dashboard → Products (产品) → click product → shown in URL and detail page - `API Key` → Dashboard → API & Development (集成) → API Keys → Create Key --- ## Test Cards ### Successful Payments | Card | Type | |------|------| | `4576 7500 0000 0110` | Visa Credit | | `2226 9000 0000 0110` | Mastercard Credit | | `4001 7000 0000 0110` | Visa Debit | | `2226 9300 0000 0110` | Mastercard Debit | ### Declined Payments | Card | Type | |------|------| | `4576 7500 0000 0220` | Visa Credit | | `2226 9000 0000 0220` | Mastercard Credit | | `4001 7000 0000 0220` | Visa Debit | | `2226 9300 0000 0220` | Mastercard Debit | Any future expiry. Any CVC. --- ## Local Development Tips 1. **Use `ngrok` for webhook tunneling** — preserves all custom HTTP headers including `X-Waffo-Signature`. localtunnel strips custom headers. ngrok preserves them. ```bash # Install: brew install ngrok/ngrok/ngrok (or https://ngrok.com/download) ngrok http 3000 ``` 2. **Idempotency** — SDK auto-generates deterministic idempotency keys from `merchantId + path + body`. Identical requests produce identical keys, so retries are safe. 3. **Test vs Prod** — Products created in test environment by default. Use `.publish()` to promote to production. Webhook events include `mode: "test" | "prod"`. --- ## Product Model Decision Table | Situation | Model | |-----------|-------| | Fixed public price | Product price set on the product | | Runtime-calculated amount (overage, credits) | 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 `priceSnapshot` | --- ## Quick Start Checklist 1. `npm install @waffo/pancake-ts` 2. Set only `WAFFO_MERCHANT_ID` and `WAFFO_PRIVATE_KEY` env vars (see "Where to find IDs" above) 3. Initialize `new WaffoPancake({ merchantId, privateKey })` 4. Create store: `client.stores.create({ name })` — or use an existing store from Dashboard 5. Before creating product(s), confirm which store should own them when the merchant has multiple stores 6. Create product(s): `client.onetimeProducts.create(...)` or `client.subscriptionProducts.create(...)` — or copy existing Product IDs from Dashboard 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, signatureHeader)` — use `request.text()` not `.json()` 10. Configure webhook URL: `client.webhooks.add({ storeId, channel: \"http\", url, events, testMode })` --- ## Documentation - Full docs: https://docs.waffo.ai/ - AI-readable full reference: https://docs.waffo.ai/llms-full.txt - SDK integration: https://docs.waffo.ai/integrate/sdks - AI Skills guide: https://docs.waffo.ai/integrate/ai-integration - npm: https://www.npmjs.com/package/@waffo/pancake-ts - Dashboard: https://pancake.waffo.ai/merchant/dashboard/integration ```` # Consumer Portal Source: https://docs.waffo.ai/merchant/customer-portal Access and configure the self-service portal for your customers ## Overview The Consumer Portal is a merchant-level feature that gives your customers a self-service interface to manage their orders, subscriptions, and billing details. It operates across all your stores. Access it from the **user menu → Consumer Portal**. *** ## What Customers Can Do | Feature | Description | | ------------------------ | ------------------------------------------------- | | View orders | Browse all past orders and payment history | | Download invoices | PDF invoices with multi-language support | | Manage subscriptions | View status, next billing date, and cancel | | Reactivate subscriptions | Resume a canceling subscription before it expires | | Request refunds | Submit refund requests for eligible payments | | Update billing details | Edit invoice address and tax information | *** ## Accessing the Portal There are two ways customers reach the portal: Customers log in via a one-time magic link sent to their email. No password required. Share your store's portal URL directly. Customers enter their email to receive a login link. *** ## Portal URL Each store has a unique portal URL: ``` https://pancake.waffo.ai/store/{store-slug} ``` Find your store slug in **Store Settings**. *** ## Merchant View From the user menu, clicking **Consumer Portal** opens a preview of the portal as it appears to your customers. Use this to verify the experience before sharing the link. The portal automatically adapts to your store's checkout theme settings (colors, dark/light mode). # Merchant Finance Source: https://docs.waffo.ai/merchant/finance Pooled balance and payouts across all your stores ## Overview Open **Merchant Finance** from the user menu (top-right avatar) or at `/merchant/dashboard/finance`. This is your merchant-level view — all your stores' settled revenue flows here, and you request payouts from here. Merchant Finance page *** ## Balance Indicators Three KPI cards at the top of the page summarize your funds: | Indicator | Meaning | | ------------------------- | ---------------------------------------------------------- | | **Available to withdraw** | Net amount that has fully settled and is ready to pay out | | **Processing** | Payments still in the clearing window (≈ 10 business days) | | **Total withdrawals** | Cumulative amount you have paid out so far | *** ## Tabs | Tab | Content | | ---------------------- | -------------------------------------------- | | **Pay out** | Request a payout from your available balance | | **Withdrawal records** | Historical payouts and their statuses | If you haven't bound a payout account yet, a banner at the top links to **Payout Accounts**. *** ## Payout Accounts Payout accounts live at the merchant level — all your stores share the same set. Manage them under [Payout Accounts](/merchant/payout-accounts). **Current coverage:** mainland China, settled in CNY, via bank card or Alipay. More regions and currencies are on the roadmap — contact support to let us know what matters most. *** ## Fees Two kinds of fees apply to the amount that lands in your account: * **Transaction fee** — deducted from each successful payment at the time of settlement. The Net column reflects what you actually receive after this. * **Payout fee** — charged when you initiate a payout from your merchant balance. See [Fee Details](/mor/fees) for the full breakdown with worked examples. # Identity Verification Source: https://docs.waffo.ai/merchant/identity-kyc Verify your identity to receive payouts Verify your identity before you add a payout account. Every payout is sent to a holder name derived from your verified identity — getting that name exactly right is how we make sure your funds arrive. This page covers **individual identity verification**, used when you pay out to a personal bank account or Alipay. If you pay out to a **company account**, see [Paying out to a company account](#paying-out-to-a-company-account) below. Identity verification is required before you can add a payout account and request payouts. ## What you'll provide Identity form on Profile — first name, last name, legal full name, country, ID type, ID number | Field | Format | Notes | | ------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | **First name** | Latin characters, 1–64 chars | Romanized given name | | **Last name** | Latin characters, 1–80 chars | Romanized family name | | **Legal full name** | Local script (e.g. 张三), 1–128 chars | The most important field. Must match your ID exactly — this becomes the holder name on every payout | | **Nationality** | ISO 3166-1 alpha-2 (e.g. CN) | Country of citizenship | | **ID type** | National ID **or** Passport | National ID for mainland China, passport for others | | **ID number** | 18 digits (National ID) or 7–13 alphanumeric (Passport) | Must match the ID type | Legal full name is the field people most often get wrong. Compare it to your ID character-by-character — including spaces and punctuation — before you save. ## When you can edit your identity Whether the core fields are editable depends on your payout history: * **No payout activity yet** — legal name, ID number, nationality, and ID type can all be edited freely. Previously empty fields can also be filled in any time. * **A payout is in progress, or you have one or more successful payouts on record** — the core fields are locked from self-edit. Contact support if you legitimately need to update one (for example, a legal name change with supporting documentation). Sensitive fields (ID number, bank account number, Alipay account) are stored encrypted. The dashboard only shows the last few characters. ## Paying out to a company account If you'll receive payouts on a **company bank account** instead of a personal one, the identity used for the holder-name match is your **company**, not you as an individual. The fields you provide are different — they describe your legal entity rather than your personal ID. **Coming soon.** Company-entity identity verification is being finalized. Until it's released, all payouts use the individual identity on your account. Watch this page for updates. ## Where to find it The identity form lives on the **Profile** page. Open it from the user menu (top-right avatar) → **Profile**. # Merchant Overview Source: https://docs.waffo.ai/merchant/overview How merchant-level features fit together ## Merchant vs Store Waffo Pancake organizes your account into two levels: Each store has its own products, orders, customers, and revenue. Manage store-level data from the sidebar inside a store. Your merchant account sits above all stores. It owns your identity, your payout accounts, and the pooled balance across every store you run. Access merchant tools from the top-right avatar menu. ## Merchant-Level Pages Open from the user menu (top-right avatar): | Page | What it's for | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **Profile** | Your personal identity used for payout holder-name matching. See [Identity Verification](/merchant/identity-kyc). | | **Merchant Finance** | Pooled balance across all your stores. Initiate payouts and view withdrawal history. See [Merchant Finance](/merchant/finance). | | **Payout Accounts** | Bank card or Alipay accounts that receive your payouts. Shared across all your stores. See [Payout Accounts](/merchant/payout-accounts). | # Payout Accounts Source: https://docs.waffo.ai/merchant/payout-accounts Set up, change, and manage your payout accounts Payout accounts are where your payouts arrive. They live at the merchant level — every store you own shares the same set of accounts. You can keep multiple on file and switch the default at any time. Payout Accounts page — basic identity summary and saved accounts ## Setting Up a Payout Account You can't add a payout account without a verified identity — the holder name on every payout is taken from your legal name. See [Identity Verification](/merchant/identity-kyc) if you haven't done this yet. Click your avatar in the top-right and select **Payout Accounts**. Choose **Bank Card** or **Alipay** and fill in the fields. Your name is filled in automatically from your identity — you don't type it here. On your first payout to a new account, the bank confirms the holder name matches your verified identity. Once matched, the account is trusted for all future payouts. Sensitive fields — ID number, bank account number, Alipay account — only show the last few characters after saving. The full value is only used when a payout is being processed. ## Available Payout Methods | Method | Fields required | | ------------- | --------------------------------------------------------------------------------------- | | **Bank Card** | Bank (selectable from the list in the dashboard), account number, purpose of remittance | | **Alipay** | Mobile number **or** email registered with Alipay, purpose of remittance | **Current coverage:** mainland China, settled in CNY. We are actively expanding to more regions and currencies — contact support to let us know what matters most to you so we can prioritize. ## Changing Your Payout Account or Default You can add a new account, edit an existing one, set a different default, or remove an account at any time. | Action | What it does | | ------------------ | ------------------------------------------------------------------------------------------------------ | | **Add** | Save a new bank card or Alipay. The same card or Alipay can't be added twice | | **Edit** | Update details on an existing account. The holder name is read-only — it tracks your verified identity | | **Set as default** | The default is used for every payout unless you switch before submitting | | **Delete** | Remove an account. Only allowed when no payouts are pending against it | Switching the default doesn't affect payouts already in flight. They land in the account you originally chose at submit time. ## Common Issues The most common cause is an incomplete identity. Without a verified identity, the form won't save. Open **Profile**, fill in the required fields, then come back. See [Identity Verification](/merchant/identity-kyc). The holder name on every payout is taken from your verified legal name — automatically. This avoids typos that would bounce a payout. If your legal name needs to change, update it on **Profile**; if it's already locked, contact support. No. If you submit a card or Alipay account that's already on file, nothing changes. Add a different account, or edit the existing one. Use Alipay, or contact support so we can prioritize adding your bank. # Payout FAQ Source: https://docs.waffo.ai/merchant/payout-faq Common payout questions As soon as your identity is verified and you've added a payout account. The first sale on your store starts a \~10 business day waiting period before funds clear into your merchant balance — once they do, you can request a payout whenever your balance hits the \$20 USD minimum. Today we pay out to merchants in **mainland China**, settled in CNY via bank card or Alipay. Most major mainland Chinese banks are supported. More currencies and countries are on the roadmap — contact support and let us know which one you need. No. Finance and Payout History always read live data, and there's no way to simulate a payout. Anything you submit moves real money. Open **Payout Accounts** from the user menu (or **Finance → Payout Account**). Add a new account and set it as default, or edit the details on an existing one. The holder name is taken from your identity and isn't editable here. See [Payout Accounts](/merchant/payout-accounts). Pending is normal for the first 3–5 business days while the bank processes it. Only worry if it stays pending past 5 business days — at that point, contact support with your payout ID. The bank returned a holder name that doesn't match your legal name on file. Open **Profile**, compare your legal name field to your ID character-by-character (including spaces and punctuation), save the correction, and retry. If the field is locked, contact support. Reach out if your payout has been pending for more than 5 business days, you see a failure reason you don't recognize, you need to change a locked identity field, or you need a country or currency we don't support yet. Include your merchant ID, the payout ID, the timestamp, and a screenshot. # Payouts Source: https://docs.waffo.ai/merchant/payout-flow How payouts work How payouts work. Every sale on your store goes through a short waiting period, then auto-settles into your merchant balance. From there, you request a payout whenever you want — funds land in your bank or Alipay a few business days later. A customer pays. The amount lands in that store's balance and waits about **10 business days** (around two weeks) so any refunds or chargebacks can settle first. After the waiting period, funds move automatically into your **merchant balance** — pooled across all of your stores. Nothing for you to do. View it in **Finance** at the top of the dashboard. Once your balance clears the minimum, you initiate a payout from Finance. Funds typically land in **3–5 business days**. ## Fees and Payout Schedule | | | | ------------------- | --------------------------------------------------------------------------------------------------- | | **Minimum payout** | **\$20 USD** — converted to your local currency at the rate when you submit | | **Settlement hold** | \~10 business days from sale before funds clear | | **Arrival time** | 3–5 business days after you request | | **Payout schedule** | On-demand — no fixed cycle; request manually once your balance clears the minimum | | **Payout fee** | **1% per payout, \$10 minimum** — deducted from the amount before arrival | | **Taxes** | Local taxes may also apply, depending on your jurisdiction. Contact support if you need a breakdown | **Current coverage:** mainland China, settled in CNY. More regions and currencies are on the roadmap. Test mode doesn't affect payouts. Finance and Payout History always show real data, and you can't simulate a payout. ## Requesting a Payout Merchant Finance — balance, available amount, and request payout Click **Finance** in the top navigation. You'll see your merchant balance and the amount available to withdraw. Type the amount you want. The minimum is **\$20 USD** — the threshold is set in USD, and the local-currency amount arriving in your account is calculated at the rate the moment you submit. Your default payout account is selected. If you've added more than one, switch before submitting. The holder name comes from your verified identity — it's not editable here. Click **Request Payout**. The payout enters **pending** and you can track it under **Payout History**. ## Failed Payouts Most payouts land without issue. If yours doesn't, work through this list: * **Name mismatch.** The bank returned a holder name that doesn't match your legal name on file. Open **Profile**, compare your legal name to your ID character-by-character (including spaces and punctuation), correct it, and retry. If the field is locked, contact support. * **Identity incomplete.** You can't request a payout — or even add a payout account — without a verified identity. See [Identity Verification](/merchant/identity-kyc). * **Below the minimum.** Payouts under \$20 USD equivalent are rejected. Wait until your balance clears the threshold, or contact support for an exception. * **Pending longer than 5 business days.** The bank may be running extra checks on a large amount or a brand-new account. If it doesn't clear within a week, contact support with your payout ID. * **Holder name doesn't match your identity.** Each payout account must belong to the same legal person whose identity you've verified. If the names don't match exactly — even a punctuation or spacing difference — the payout fails. Open **Profile**, verify your legal name matches your ID exactly, and retry. See [Identity Verification](/merchant/identity-kyc). When contacting support, include your merchant ID, the payout ID, the timestamp, and a screenshot. That's enough for us to find the record and respond fast. ## Managing Your Payout Account You can add, switch, or change your default payout account at any time. See [Payout Accounts](/merchant/payout-accounts) for the setup walkthrough and supported methods. Add a bank card or Alipay and manage your default Verify your identity before you can receive a payout # Preferences Source: https://docs.waffo.ai/merchant/preferences Manage display currency, language, and appearance for your dashboard ## Overview Preferences are personal settings that affect how the dashboard displays for you. They are not store-specific and apply across all stores in your merchant account. Access preferences from the **user menu** (click your avatar in the top-right corner). *** ## Available Preferences ### Display Currency The currency used to display amounts throughout the dashboard. * Default: **USD** * You can switch to any supported currency for display purposes * This does **not** affect your actual settlement currency or payout currency USD is the official settlement currency. Other display currencies are for reference only and use live exchange rates. ### Language The dashboard interface language. | Option | Language | | ------- | ------------------ | | English | English | | 简体中文 | Simplified Chinese | | 日本語 | Japanese | ### Appearance The dashboard color theme. | Option | Description | | ---------- | ------------------------------------ | | **Light** | Light background theme | | **Dark** | Dark background theme | | **System** | Follows your device's system setting | # Account Reviews Source: https://docs.waffo.ai/mor/account-reviews What we review before enabling live payments, and how to pass it As a Merchant of Record, Waffo Pancake processes payments and handles tax on your behalf. Before a store can accept **live payments**, we review it once to confirm it's a legitimate business that complies with our terms. This protects you, your customers, and the payment network. You can build products and take **test payments** at any time without a review. The review only gates **live payments** on a store. ## What gets reviewed The review applies to a **store** (the Store Business Review). You submit your business information from the store's [Business Details](/merchant/business-details) page, and our team reviews it. Identity information for [payouts](/merchant/payout-accounts) is **not** part of this review and has no separate approval step. ## Information you provide When you submit a store for review, you provide: * **Legal name** — the individual or business entity behind the store * **Business description** — what your business does and how it operates * **Product website URL** — a live, publicly reachable page describing what you sell * **Contact email** — a real, monitored support address * **Business type and tax residence** — for correct tax handling * Confirmation that your product complies with our terms (prohibited products, pricing visibility, no trademark conflicts) ## Approval checklist Go through every item below before you submit. Most "changes requested" outcomes come from missing one of these — fixing them upfront gets you approved on the first pass. * [ ] **Product is live** — Your product is ready for production and reachable at the URL you submit, with no login wall, password, or "coming soon" placeholder. Still building? Take [test payments](/features/test-mode) in Test Mode first, then submit when it's ready. * [ ] **Product is clearly visible** — We can understand what you're selling from your website or landing page. The product, its purpose, and how it works are obvious to a first-time visitor. * [ ] **Pricing is visible** — What customers will be charged is clearly displayed and easy to find — before they reach checkout. * [ ] **Privacy Policy & Terms of Service** — Both legal pages exist and are reachable on your website without logging in. Need a T\&C? Use our [Terms of Service template](/compliance/tos). * [ ] **Customer support email** — A real, monitored support email is set up and shown on your website, and it matches the contact email you submit for review. The same address should appear on customer receipts. * [ ] **No false information** — No fake reviews, testimonials, or inflated user/customer counts. Everything on your site is truthful. * [ ] **No trademark conflicts** — Your product and store name don't infringe an existing trademark or cause confusion with another brand. * [ ] **Acceptable use** — No high-risk, deceptive, or illegitimate use cases. * [ ] **Not on the prohibited list** — Your product isn't on our [prohibited products](/mor/prohibited-products) list. ## Review outcomes Review typically takes **1–3 business days**. You can track the status from the Business Details page. There are two outcomes: | Outcome | What it means | What to do | | --------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Approved** | The store can now accept live payments (`prodEnabled`). | Start selling. | | **Changes requested** | Something needs fixing before approval. | Address the feedback and resubmit — see [Resubmitting after changes requested](#resubmitting-after-changes-requested). |

Resubmitting after changes requested

If the review comes back with changes requested, you don't start over — fix the flagged items and resubmit the same store. Common reasons and fixes: | Reason | Fix | | ---------------------------- | -------------------------------------------------------------------------------- | | Website inaccessible | Remove password protection or bot walls so the page loads for anyone. | | Missing or gated legal pages | Add a Terms of Service and Privacy Policy reachable without login. | | Support email mismatch | Make the email in your store settings match the one on your website. | | Product not live | Remove "coming soon" / placeholder content; the product must be real and usable. | | Inaccurate information | Correct anything misleading — descriptions, reviews, or claims must be truthful. | Work through the feedback on the Business Details page. Edit the submission with the corrected information. Submit again from the Business Details page. The status resets and the store re-enters review (typically another 1–3 business days). ## Frequently asked questions Typically 1–3 business days after submission. Track the status from the Business Details page. You can test the full flow in test mode anytime. Live payments require the store review to pass. Fix the flagged items and resubmit the same store. The status resets and it goes through review again. # AIGC Acceptable Use Policy Template Source: https://docs.waffo.ai/mor/account-reviews/aup A content policy for AI products on Pancake — content standards, moderation, reporting, and enforcement A complete Acceptable Use Policy for AI-powered (AIGC) products on Pancake. It sets the content standards, moderation, reporting, and enforcement that keep an AI product in good standing — pick what applies, replace the placeholders, publish. This is a template, not legal advice. AI content rules vary by market and change fast — have a qualified AI-compliance professional review it before publishing if your model, content types, or jurisdiction are unusual. ## Why this matters for AI products If you resell or build on top of AI models, [account review](/mor/account-reviews) looks at how you handle the content your users generate. A published AUP plus real moderation is what keeps an AI product approved — and keeps it from being suspended later. The four things review cares about most: **Required:** your own product name and identity. **Not allowed:** marketing yourself *as* an AI model brand (e.g., "the official GPT app") or using a model's name/logo as your own. **Required:** describe what your product actually does. **Not allowed:** overstating capabilities, implying endorsement by a model provider, or hiding that output is AI-generated. **Required:** run a content-moderation step (a moderation API or equivalent) on inputs and outputs. **Not allowed:** shipping an unmoderated raw-model passthrough. **Required:** a public AUP (this template) covering prohibited content, reporting, and enforcement. **Not allowed:** no policy, or one that doesn't match your actual product. ## Before you publish or request review 🔴 items decide whether an AI product is approved. 🟡 items strengthen the case. * Product uses **independent branding** — not an AI model's name or logo as your own * Marketing is accurate — no overstated capabilities, no implied provider endorsement * A **moderation step runs on inputs and outputs** (name the tool in Section 3.1) * The "Absolutely Prohibited" list (2.1) is published and enforced — CSAM, terrorism, WMD, non-consensual deepfakes * Prohibited and restricted categories match what your product can actually generate * At least one **real, monitored reporting channel** exists (Section 5) * Enforcement actions (Section 7) are ones you can actually carry out * Output is labeled or otherwise disclosed as AI-generated * Severity tiers (3.4) map to concrete response times (Section 6) * An appeals path exists for wrongly-actioned users (Section 8) * Age verification in place if you allow any restricted (e.g., adult) content * Every `[placeholder]` replaced with real text * "Last Updated" date filled in * Categories/channels that don't apply to your product are deleted, not left as stubs This AUP is the standalone content policy. It complements Section 10 (Acceptable Use) of the [Terms of Service template](/mor/account-reviews/tos) — the T\&C points users at your full content policy; this is that policy.

Template

Copy the block below and replace every `[placeholder]` with your real text. Delete any category, channel, or action that doesn't apply to your product. The template is in English. Translate or localize as your market requires — the content standards should stay the same. ```text Acceptable Use Policy — AI-Generated Content (replace every [placeholder]) theme={"system"} [Platform / Company Name] Acceptable Use Policy — AI-Generated Content Last Updated: [DATE] [Platform / Company Name] ("Platform," "we," "us," or "our") is committed to providing safe, lawful, and responsible AI-generated content services. This Acceptable Use Policy ("Policy") applies to all content generated, published, distributed, or stored through the Platform, and sets out our content standards, moderation, reporting procedures, and enforcement actions. All users, developers, and merchants using the Platform must comply with this Policy. 1. Scope This Policy applies to all content types, including: - Text: AI-generated text, dialogue, code, articles, etc. - Images: AI-generated or edited images and illustrations. - Audio / Video (if applicable): AI-synthesized voice, music, and video. - User Inputs (Prompts): prompts and instructions submitted by users. - [Other content types supported by the Platform, if applicable.] 2. Content Standards & Prohibited Categories 2.1 Absolutely Prohibited — Zero Tolerance The following will be actioned immediately and may be reported to authorities: - Child sexual abuse material (CSAM) or any sexual content involving minors. - Content that promotes or facilitates terrorism, extremism, or mass violence. - Technical instructions for weapons of mass destruction. - Content inciting genocide or ethnic hatred. - Non-consensual deepfake sexual content targeting real individuals. - [Add categories based on applicable local regulations.] 2.2 Restricted — Subject to Review - Adult content: only permitted on age-verified, compliant platforms. - Violent content: must have a clear creative or educational context. - Politically sensitive content: content that may be misleading. - Medical / legal / financial advice: must comply with local regulations. 2.3 Generally Prohibited Conduct - Generating or spreading misinformation or disproven claims. - Impersonating real individuals, brands, or organizations. - Infringing intellectual property, publicity, or privacy rights. - Deceiving users by passing AI-generated content off as human-created. - Bulk-generating spam or deceptive commercial content. - Jailbreak prompts designed to bypass Platform safety measures. 3. Content Moderation We operate a multi-layer moderation system: automated review, human review, and periodic audits. 3.1 Automated Review (real-time) All requests and generated content are scanned in real time by safety filters. - Input filtering: keyword, semantic, and intent detection on prompts. - Output filtering: real-time safety assessment of generated content. - Usage pattern analysis: monitoring for anomalous API usage. - Safety model used: [e.g., OpenAI Moderation API, or a proprietary model]. 3.2 Human Review - Auto-flagged content enters a human review queue. - User-reported content is reviewed manually by the trust & safety team. - Review team: [e.g., 24/7 on-call moderation team]. - Reviewers complete content-safety training and sign NDAs. 3.3 Periodic Audits - Random content sampling performed [specify frequency]. - Regular evaluation and improvement of classifier accuracy. - [Whether independent third-party audits are conducted, if applicable.] 3.4 Content Severity Levels | Level | Description | Review | Action | |---|---|---|---| | L1 — Critical | Child safety, terrorism | Auto-block + human confirmation | Immediate ban + report | | L2 — High | Severe violations | Auto-flag + priority review | Remove + suspend account | | L3 — Medium | General violations | Human review | Warning + removal | | L4 — Low | Minor violations | Report-triggered | Warning + edit required | 4. Reporting 4.1 When to Report - Content or behavior violating this Policy's content standards. - Critical violations involving child safety or terrorism. - AI impersonation or infringement of your own rights. - [Other situations the Platform deems reportable.] 4.2 Reporting Principles - Confidentiality: reporter identity is strictly protected. - Impartiality: all reports receive independent, objective review. - Anti-abuse: malicious or false reports are recorded and actioned. 5. Reporting Channels Retain the options that apply: - Dedicated report email: [report@yourplatform.com] - Online report form: [https://yourplatform.com/report] - In-product report button: [e.g., Content page → ··· → Report] - Customer support: [live chat or support email] - Emergency contact (critical): [hotline or instant-messaging channel, if applicable] 6. Response & Processing Timelines | Violation Type | Initial Response | Resolution | |---|---|---| | L1 — Critical | Within 2 hours | Within 24 hours | | L2 — High | Within 24 hours | Within 3 business days | | L3 — Medium | Within 3 business days | Within 7 business days | | L4 — Low | Within 5 business days | Within 15 business days | If extended processing is required, we will notify the reporter proactively with reasons. Processing workflow: received (auto-acknowledge) → triage (classify L1–L4) → review (auto + human) → decision → enforcement → notification. 7. Enforcement Actions | Action | Measure | Applies to | |---|---|---| | 1. Warning | Written notice requiring removal or edit | First-time minor violation | | 2. Content removal | Violating content taken down | Confirmed L3–L4 | | 3. Feature restriction | Reduced quota or model access | Repeat minor or single L3 | | 4. Account suspension | Temporary suspension pending review | L2 or cumulative violations | | 5. Permanent ban | Permanent termination of access | L1 or repeat L2 | | 6. Legal action | Report to authorities; cooperate with investigation | Criminal-level L1 | 8. Appeals You may appeal an enforcement decision within [specify days] of receiving notice: - Appeal channel: [appeal@yourplatform.com] - Timeline: [e.g., reviewed within 10 business days]. - Review method: reviewed by a reviewer independent of the original decision. 9. Transparency & Policy Updates - We publish a content-safety transparency report every [specify period]. - Material changes are notified at least [e.g., 15] days in advance. - Continued use of the Platform constitutes acceptance of updates. 10. Contact Us - Safety Email: [safety@yourplatform.com] - Report Email: [report@yourplatform.com] - Appeal Email: [appeal@yourplatform.com] - Company Name: [full legal name] - Address: [registered address] This Policy is for general reference only and does not constitute legal advice. We recommend having it reviewed by a qualified AI-compliance attorney before publication. ``` ## Section-by-section reference Each section below shows its **tier** (🔴 load-bearing · 🟡 strongly recommended · ⚪ optional), a one-line summary, and the matching **template snippet** so you can read the guidance and copy that block together.

1\. Scope

**🔴 load-bearing** Names every content type you generate. A code assistant doesn't need the image/audio lines; an image generator does. Prompts count as content too — keep that line. ```text Section 1 theme={"system"} 1. Scope This Policy applies to all content types, including: - Text: AI-generated text, dialogue, code, articles, etc. - Images: AI-generated or edited images and illustrations. - Audio / Video (if applicable): AI-synthesized voice, music, and video. - User Inputs (Prompts): prompts and instructions submitted by users. - [Other content types supported by the Platform, if applicable.] ```

2\. Content Standards & Prohibited Categories

**🔴 load-bearing** The 2.1 zero-tolerance list is the floor, not a menu — keep all of it and add local-law items. 2.2 is "allowed with conditions" (age-gating, context, local rules); if you have no age verification, delete the adult-content line. 2.3's "passing AI off as human" and "jailbreak" lines are AI-specific and matter for review. ```text Section 2 theme={"system"} 2. Content Standards & Prohibited Categories 2.1 Absolutely Prohibited — Zero Tolerance The following will be actioned immediately and may be reported to authorities: - Child sexual abuse material (CSAM) or any sexual content involving minors. - Content that promotes or facilitates terrorism, extremism, or mass violence. - Technical instructions for weapons of mass destruction. - Content inciting genocide or ethnic hatred. - Non-consensual deepfake sexual content targeting real individuals. - [Add categories based on applicable local regulations.] 2.2 Restricted — Subject to Review - Adult content: only permitted on age-verified, compliant platforms. - Violent content: must have a clear creative or educational context. - Politically sensitive content: content that may be misleading. - Medical / legal / financial advice: must comply with local regulations. 2.3 Generally Prohibited Conduct - Generating or spreading misinformation or disproven claims. - Impersonating real individuals, brands, or organizations. - Infringing intellectual property, publicity, or privacy rights. - Deceiving users by passing AI-generated content off as human-created. - Bulk-generating spam or deceptive commercial content. - Jailbreak prompts designed to bypass Platform safety measures. ```

3\. Content Moderation

**🔴 load-bearing** How you actually catch violations — review expects a real moderation step, not a promise. Name a real tool in 3.1 (`[OpenAI Moderation API]` or your own); a step on inputs **and** outputs is the single thing review most wants to see. The L1–L4 tiers in 3.4 drive response times and enforcement. ```text Section 3 theme={"system"} 3. Content Moderation We operate a multi-layer moderation system: automated review, human review, and periodic audits. 3.1 Automated Review (real-time) All requests and generated content are scanned in real time by safety filters. - Input filtering: keyword, semantic, and intent detection on prompts. - Output filtering: real-time safety assessment of generated content. - Usage pattern analysis: monitoring for anomalous API usage. - Safety model used: [e.g., OpenAI Moderation API, or a proprietary model]. 3.2 Human Review - Auto-flagged content enters a human review queue. - User-reported content is reviewed manually by the trust & safety team. - Review team: [e.g., 24/7 on-call moderation team]. - Reviewers complete content-safety training and sign NDAs. 3.3 Periodic Audits - Random content sampling performed [specify frequency]. - Regular evaluation and improvement of classifier accuracy. - [Whether independent third-party audits are conducted, if applicable.] 3.4 Content Severity Levels | Level | Description | Review | Action | |---|---|---|---| | L1 — Critical | Child safety, terrorism | Auto-block + human confirmation | Immediate ban + report | | L2 — High | Severe violations | Auto-flag + priority review | Remove + suspend account | | L3 — Medium | General violations | Human review | Warning + removal | | L4 — Low | Minor violations | Report-triggered | Warning + edit required | ```

4\. Reporting

**🔴 load-bearing** When and how users flag content, plus confidentiality and anti-abuse. The "independent, objective review" and "false reports actioned" lines keep the process credible. ```text Section 4 theme={"system"} 4. Reporting 4.1 When to Report - Content or behavior violating this Policy's content standards. - Critical violations involving child safety or terrorism. - AI impersonation or infringement of your own rights. - [Other situations the Platform deems reportable.] 4.2 Reporting Principles - Confidentiality: reporter identity is strictly protected. - Impartiality: all reports receive independent, objective review. - Anti-abuse: malicious or false reports are recorded and actioned. ```

5\. Reporting Channels

**🔴 load-bearing** At least one real, monitored channel — list only what you can staff. One real channel beats five unstaffed ones. ```text Section 5 theme={"system"} 5. Reporting Channels Retain the options that apply: - Dedicated report email: [report@yourplatform.com] - Online report form: [https://yourplatform.com/report] - In-product report button: [e.g., Content page → ··· → Report] - Customer support: [live chat or support email] - Emergency contact (critical): [hotline or instant-messaging channel, if applicable] ```

6\. Response & Processing Timelines

**🟡 strongly recommended** Response/resolution windows by severity. Be realistic — you'll be held to them. Keep L1 fast (hours, not days); child-safety and terrorism content can't sit in a queue. ```text Section 6 theme={"system"} 6. Response & Processing Timelines | Violation Type | Initial Response | Resolution | |---|---|---| | L1 — Critical | Within 2 hours | Within 24 hours | | L2 — High | Within 24 hours | Within 3 business days | | L3 — Medium | Within 3 business days | Within 7 business days | | L4 — Low | Within 5 business days | Within 15 business days | If extended processing is required, we will notify the reporter proactively with reasons. Processing workflow: received (auto-acknowledge) → triage (classify L1–L4) → review (auto + human) → decision → enforcement → notification. ```

7\. Enforcement Actions

**🔴 load-bearing** The escalation ladder from warning to permanent ban to legal referral. List only actions you can actually take — if you can't suspend at the account level, describe what you can do instead. ```text Section 7 theme={"system"} 7. Enforcement Actions | Action | Measure | Applies to | |---|---|---| | 1. Warning | Written notice requiring removal or edit | First-time minor violation | | 2. Content removal | Violating content taken down | Confirmed L3–L4 | | 3. Feature restriction | Reduced quota or model access | Repeat minor or single L3 | | 4. Account suspension | Temporary suspension pending review | L2 or cumulative violations | | 5. Permanent ban | Permanent termination of access | L1 or repeat L2 | | 6. Legal action | Report to authorities; cooperate with investigation | Criminal-level L1 | ```

8\. Appeals

**🟡 strongly recommended** A second look by someone uninvolved in the original decision. The "independent reviewer" line is the point — appeals reviewed by the same person who made the call aren't appeals. ```text Section 8 theme={"system"} 8. Appeals You may appeal an enforcement decision within [specify days] of receiving notice: - Appeal channel: [appeal@yourplatform.com] - Timeline: [e.g., reviewed within 10 business days]. - Review method: reviewed by a reviewer independent of the original decision. ```

9\. Transparency & Policy Updates

**⚪ optional** Optional reporting cadence plus a change-notice window. The transparency report is optional; the change-notice window is good practice. ```text Section 9 theme={"system"} 9. Transparency & Policy Updates - We publish a content-safety transparency report every [specify period]. - Material changes are notified at least [e.g., 15] days in advance. - Continued use of the Platform constitutes acceptance of updates. ```

10\. Contact Us

**🔴 load-bearing** Real safety/report/appeal inboxes. Every channel you list is one you're committing to monitor. ```text Section 10 theme={"system"} 10. Contact Us - Safety Email: [safety@yourplatform.com] - Report Email: [report@yourplatform.com] - Appeal Email: [appeal@yourplatform.com] - Company Name: [full legal name] - Address: [registered address] ``` Publish the AUP at a stable URL and link it from your product (footer, signup, and the report button). Reference it from Section 10 of your [Terms of Service](/mor/account-reviews/tos) so the two documents point at each other. # Privacy Policy Template Source: https://docs.waffo.ai/mor/account-reviews/privacy-policy A drop-in privacy policy for Pancake merchants — GDPR/CCPA-aware, ready to localize A complete, regulation-aware privacy policy for Pancake merchants. Fill in your details, replace the bracketed placeholders, publish — and link it from your checkout and your Terms of Service. This is a template, not legal advice. Privacy law varies by market — have a qualified professional review it before publishing if your jurisdiction or data practices are unusual. ## Before you publish 🔴 items are legally load-bearing. 🟡 items materially reduce your privacy risk. * Data controller filled in — legal company name, registered address, privacy contact email * Every category of data you actually collect is listed (delete rows you don't use; add ones you do) * Payment processor named: **`Waffo Pancake`** — card data is processed by Pancake, not stored on your servers * "We do not sell your personal information" kept if true (it's true for most Pancake merchants) * Retention periods filled in with real numbers * Children's minimum age set to match your market (13 / 16 / 18) * The policy is linked from checkout and referenced by your Terms of Service `[privacy policy URL]` * Analytics tools named, each linked to its own privacy policy * International-transfer safeguards declared if your servers or vendors are cross-border (SCCs, adequacy, BCRs) * A real opt-out path for marketing (unsubscribe link + account toggle), separate from essential service notices * Breach-notification timeframe stated (e.g., 72 hours) * Every `[placeholder]` replaced with real text * "Last Updated" date filled in * Optional blocks (location, third-party login, platform/API data sharing) kept only if they apply **Keep it in sync with your T\&C.** Section 11 of the [Terms of Service template](/mor/account-reviews/tos) links to this policy by URL — publish this one first, then paste its URL into the T\&C.

Template

Copy the block below and replace every `[placeholder]` with your real text. Delete any row or section that doesn't apply to your product. The template is in English. Translate or localize as your market requires — the legal substance should stay the same. ```text Privacy Policy (replace every [placeholder]) theme={"system"} [Legal Name] Privacy Policy Last Updated: [DATE] [Legal Name] ("we," "us," or "our") is committed to protecting your privacy. This Privacy Policy ("Policy") explains how we collect, use, store, and share your personal information when you use [Product / Service Name] (the "Service"), and the rights available to you. Please read this Policy carefully before using the Service. By using the Service, you agree to this Policy. We may update this Policy periodically and will notify you of material changes via [notification method, e.g., email or in-product notice]. 1. Data Controller The data controller for this Service is: - Company Name: [full legal company name] - Registered Address: [business registration address] - Privacy Email: [privacy@yourcompany.com] - Data Protection Officer (DPO): [name / contact, or "Not applicable"] 2. Personal Information We Collect 2.1 Information You Provide Directly - Account Info: name, email, password (stored encrypted), [other registration fields]. - Payment Info: transaction amount and payment status. Full card numbers are not stored by us — card data is processed by our payment processor (see Section 5). - Communications: emails, support tickets, and feedback you send us. - [Other business-specific fields, e.g., delivery address, business license.] 2.2 Information We Collect Automatically - Device & Network: IP address, device model, operating system, browser type, time zone. - Usage Data: pages visited, feature usage, activity logs, session duration. - Log Data: request timestamps, error logs, performance metrics. - [Other business-specific data, e.g., search history, playback records.] [Optional] - Location: collected with your authorization, for [describe purpose]. - Third-Party Login: basic profile information shared by [WeChat / Google / etc.] when you sign in through them. 3. How We Use Your Information | Purpose | Legal Basis | |---|---| | Service delivery & maintenance | Contract performance | | Billing & payment processing | Contract performance | | Customer support | Contract / Legitimate interests | | Service notifications (billing, security, policy) | Legitimate interests | | Security & fraud prevention | Legitimate interests | | Product analytics & improvement | Legitimate interests | | Legal compliance | Legal obligation | | Marketing — [describe content] (optional) | Your consent | We may aggregate or anonymize data for statistical purposes. Such data cannot be linked to any individual. 4. Cookies & Tracking Technologies | Type | Purpose | Disableable | |---|---|---| | Strictly necessary | Login sessions, core functionality | No | | Functional | Language preferences, personalization | Yes | | Analytics | Anonymous usage stats, product optimization | Yes | | Marketing (optional) | Targeted ads and effectiveness measurement | Yes | Analytics tools in use: [list tools and link to their privacy policies, e.g., Google Analytics]. You can manage your preferences via your browser settings or our Cookie Preference Center. 5. Sharing & Disclosure We do not sell your personal information, including as defined under applicable laws such as the CCPA. We share your information only in the following circumstances: - Service Providers: cloud, payment, support, and analytics vendors, bound by confidentiality. Payment card data is processed exclusively by our PCI-DSS certified payment processor, [Waffo Pancake], and is not stored on our servers. - Business Partners: [if applicable, describe partner type and data scope; otherwise delete]. - Legal Requirements: where required by law, court order, or a lawful regulatory request. - Business Transactions: in a merger, acquisition, or similar event, with advance notice and continued protections. - With Your Consent: for any other purpose, with your explicit prior consent. [Optional: For platform businesses, add a note on data sharing with counterparties. For third-party API integrations, describe the data flow to those providers.] 6. Data Security - Encryption in transit: TLS / HTTPS. - Secure storage: passwords and sensitive data are encrypted or hashed. - Access controls: least-privilege principle; staff sign confidentiality agreements. - Regular security audits and vulnerability assessments. - [Additional measures, e.g., ISO 27001, SOC 2.] In the event of a security incident affecting your rights, we will notify you and the relevant authorities within [timeframe, e.g., 72 hours of discovery] as required by law. Please keep your credentials secure and do not share them. 7. Data Retention | Data Type | Retention Period | Upon Expiry | |---|---|---| | Account information | While active; [X] years after deletion | Delete or anonymize | | Transaction records | Per regulations, typically [X] years | Delete or archive | | Support records | [X] years | Secure deletion | | Security audit logs | [X] months | Secure deletion | | [Other data type] | [period] | [method] | 8. Your Data Rights To exercise any right below, contact us; we respond within [e.g., 30 calendar days]. | Right | Description | |---|---| | Right to be informed | Know what data we collect and how we use it | | Right of access | Obtain a copy of your personal information | | Right to rectification | Correct inaccurate or incomplete data | | Right to erasure | Request deletion under certain conditions | | Right to restrict processing | Temporarily suspend processing in certain cases | | Right to data portability | Receive your data in a machine-readable format | | Right to object | Object to processing based on legitimate interests or marketing | | Right to withdraw consent | Withdraw consent for consent-based processing | You may also lodge a complaint with your local data protection authority. 9. Marketing & Opt-Out With your consent, we may send marketing communications about [describe content types] via email, SMS, or in-app notifications. You can opt out at any time: click "Unsubscribe" in any email, disable marketing in your account settings, or contact us. Opting out does not affect essential service notifications (e.g., billing, security alerts). 10. International Data Transfers Our servers and partners may be located in [list regions, e.g., Singapore, the United States]. For international transfers, we safeguard your data through: - Data processing agreements incorporating EU Standard Contractual Clauses (SCCs). - Transfers only to recipients with an equivalent level of protection. - [Other safeguards, e.g., adequacy decisions, BCRs.] 11. Children's Privacy The Service is intended for users aged [13 / 16 / 18] and above. We do not knowingly collect information from children below that age. If you believe your child has provided information, contact us immediately and we will promptly delete it. 12. Third-Party Links & Services The Service may include links to, or integrations with, third-party services. This Policy applies only to data we directly collect. We are not responsible for third-party privacy practices and encourage you to review their policies before use. 13. Policy Changes For material changes, we will provide at least [X, e.g., 15] days' advance notice via platform announcement or your registered email, and update the "Last Updated" date at the top of this page. Continued use after the effective date constitutes acceptance. 14. Contact Us - Privacy Email: [privacy@yourcompany.com] - Support Email: [support@yourcompany.com] - Company Name: [full legal name] - Mailing Address: [postal address] - Business Hours: [e.g., Mon–Fri, 09:00–18:00 UTC+8] This Privacy Policy is for general reference only and does not constitute legal advice. We strongly recommend having it reviewed by a qualified legal professional in your target market before publication. [Legal Name] · [Website URL] ``` ## Section-by-section reference Each section below shows its **tier** (🔴 load-bearing · 🟡 strongly recommended · ⚪ optional), a one-line summary, and the matching **template snippet** so you can read the guidance and copy that block together.

1\. Data Controller

**🔴 load-bearing** Names the legal entity responsible for the data — a brand name alone won't do. Fill `[full legal company name]`, `[registered address]`, `[privacy@yourcompany.com]`. DPO only applies to large-scale or sensitive processing; otherwise write "Not applicable." ```text Section 1 theme={"system"} 1. Data Controller The data controller for this Service is: - Company Name: [full legal company name] - Registered Address: [business registration address] - Privacy Email: [privacy@yourcompany.com] - Data Protection Officer (DPO): [name / contact, or "Not applicable"] ```

2\. Personal Information We Collect

**🔴 load-bearing** List only what you actually collect — over-claiming creates obligations you can't meet. Delete categories you don't use, add ones you do. Keep the line that card numbers are **not** stored by you. Drop the optional block unless location or third-party login apply. ```text Section 2 theme={"system"} 2. Personal Information We Collect 2.1 Information You Provide Directly - Account Info: name, email, password (stored encrypted), [other registration fields]. - Payment Info: transaction amount and payment status. Full card numbers are not stored by us — card data is processed by our payment processor (see Section 5). - Communications: emails, support tickets, and feedback you send us. - [Other business-specific fields, e.g., delivery address, business license.] 2.2 Information We Collect Automatically - Device & Network: IP address, device model, operating system, browser type, time zone. - Usage Data: pages visited, feature usage, activity logs, session duration. - Log Data: request timestamps, error logs, performance metrics. - [Other business-specific data, e.g., search history, playback records.] [Optional] - Location: collected with your authorization, for [describe purpose]. - Third-Party Login: basic profile information shared by [WeChat / Google / etc.] when you sign in through them. ```

3\. How We Use Your Information

**🔴 load-bearing** Each purpose is paired with a legal basis (GDPR Art. 6) — keep the pairing, it's what regulators check. If you do marketing, "Your consent" is the basis, which means a real opt-in. ```text Section 3 theme={"system"} 3. How We Use Your Information | Purpose | Legal Basis | |---|---| | Service delivery & maintenance | Contract performance | | Billing & payment processing | Contract performance | | Customer support | Contract / Legitimate interests | | Service notifications (billing, security, policy) | Legitimate interests | | Security & fraud prevention | Legitimate interests | | Product analytics & improvement | Legitimate interests | | Legal compliance | Legal obligation | | Marketing — [describe content] (optional) | Your consent | We may aggregate or anonymize data for statistical purposes. Such data cannot be linked to any individual. ```

4\. Cookies & Tracking Technologies

**🟡 strongly recommended** Disclose tracking. Strictly-necessary cookies can't be disabled; everything else must be. Name your analytics tools and link each to its own policy; marketing pixels go in the "Marketing" row. ```text Section 4 theme={"system"} 4. Cookies & Tracking Technologies | Type | Purpose | Disableable | |---|---|---| | Strictly necessary | Login sessions, core functionality | No | | Functional | Language preferences, personalization | Yes | | Analytics | Anonymous usage stats, product optimization | Yes | | Marketing (optional) | Targeted ads and effectiveness measurement | Yes | Analytics tools in use: [list tools and link to their privacy policies, e.g., Google Analytics]. You can manage your preferences via your browser settings or our Cookie Preference Center. ```

5\. Sharing & Disclosure

**🔴 load-bearing** "We don't sell" plus a named processor. For Pancake merchants the payment-processor line is the important one: **`Waffo Pancake`** is your PCI-DSS processor and card data never touches your servers. Keep "we do not sell" only if it's true. ```text Section 5 theme={"system"} 5. Sharing & Disclosure We do not sell your personal information, including as defined under applicable laws such as the CCPA. We share your information only in the following circumstances: - Service Providers: cloud, payment, support, and analytics vendors, bound by confidentiality. Payment card data is processed exclusively by our PCI-DSS certified payment processor, [Waffo Pancake], and is not stored on our servers. - Business Partners: [if applicable, describe partner type and data scope; otherwise delete]. - Legal Requirements: where required by law, court order, or a lawful regulatory request. - Business Transactions: in a merger, acquisition, or similar event, with advance notice and continued protections. - With Your Consent: for any other purpose, with your explicit prior consent. [Optional: For platform businesses, add a note on data sharing with counterparties. For third-party API integrations, describe the data flow to those providers.] ```

6\. Data Security

**🔴 load-bearing** Concrete measures plus a breach-notification window. List what you actually do; the 72-hour window (the GDPR reference point) signals you have an incident process. ```text Section 6 theme={"system"} 6. Data Security - Encryption in transit: TLS / HTTPS. - Secure storage: passwords and sensitive data are encrypted or hashed. - Access controls: least-privilege principle; staff sign confidentiality agreements. - Regular security audits and vulnerability assessments. - [Additional measures, e.g., ISO 27001, SOC 2.] In the event of a security incident affecting your rights, we will notify you and the relevant authorities within [timeframe, e.g., 72 hours of discovery] as required by law. Please keep your credentials secure and do not share them. ```

7\. Data Retention

**🔴 load-bearing** How long, then what happens. Vague "as long as necessary" is weak — give numbers. Tie transaction-record retention to your tax/accounting obligations, not a guess. ```text Section 7 theme={"system"} 7. Data Retention | Data Type | Retention Period | Upon Expiry | |---|---|---| | Account information | While active; [X] years after deletion | Delete or anonymize | | Transaction records | Per regulations, typically [X] years | Delete or archive | | Support records | [X] years | Secure deletion | | Security audit logs | [X] months | Secure deletion | | [Other data type] | [period] | [method] | ```

8\. Your Data Rights

**🔴 load-bearing** The eight GDPR rights, how to exercise them, and the DPA complaint route. Fill the response window (`[30 calendar days]` is the GDPR default) and make sure the privacy email in Section 14 routes these requests to someone. ```text Section 8 theme={"system"} 8. Your Data Rights To exercise any right below, contact us; we respond within [e.g., 30 calendar days]. | Right | Description | |---|---| | Right to be informed | Know what data we collect and how we use it | | Right of access | Obtain a copy of your personal information | | Right to rectification | Correct inaccurate or incomplete data | | Right to erasure | Request deletion under certain conditions | | Right to restrict processing | Temporarily suspend processing in certain cases | | Right to data portability | Receive your data in a machine-readable format | | Right to object | Object to processing based on legitimate interests or marketing | | Right to withdraw consent | Withdraw consent for consent-based processing | You may also lodge a complaint with your local data protection authority. ```

9\. Marketing & Opt-Out

**🟡 strongly recommended** Consent in, easy opt-out, and a clear line between marketing and essential notices. Keep billing and security notices outside the opt-out. ```text Section 9 theme={"system"} 9. Marketing & Opt-Out With your consent, we may send marketing communications about [describe content types] via email, SMS, or in-app notifications. You can opt out at any time: click "Unsubscribe" in any email, disable marketing in your account settings, or contact us. Opting out does not affect essential service notifications (e.g., billing, security alerts). ```

10\. International Data Transfers

**🟡 strongly recommended** Only needed if data crosses borders. Name the safeguard — SCCs cover most cases. ```text Section 10 theme={"system"} 10. International Data Transfers Our servers and partners may be located in [list regions, e.g., Singapore, the United States]. For international transfers, we safeguard your data through: - Data processing agreements incorporating EU Standard Contractual Clauses (SCCs). - Transfers only to recipients with an equivalent level of protection. - [Other safeguards, e.g., adequacy decisions, BCRs.] ```

11\. Children's Privacy

**🔴 load-bearing** Set the minimum age for your market and don't knowingly collect below it (13 US/COPPA, 16 default GDPR, varies by member state). ```text Section 11 theme={"system"} 11. Children's Privacy The Service is intended for users aged [13 / 16 / 18] and above. We do not knowingly collect information from children below that age. If you believe your child has provided information, contact us immediately and we will promptly delete it. ```

12\. Third-Party Links & Services

**⚪ optional** Disclaims responsibility for sites and services you don't control. ```text Section 12 theme={"system"} 12. Third-Party Links & Services The Service may include links to, or integrations with, third-party services. This Policy applies only to data we directly collect. We are not responsible for third-party privacy practices and encourage you to review their policies before use. ```

13\. Policy Changes

**🔴 load-bearing** Advance-notice window plus updating the date. 15 days is a reasonable default. ```text Section 13 theme={"system"} 13. Policy Changes For material changes, we will provide at least [X, e.g., 15] days' advance notice via platform announcement or your registered email, and update the "Last Updated" date at the top of this page. Continued use after the effective date constitutes acceptance. ```

14\. Contact Us

**🔴 load-bearing** Real, monitored channels. The privacy email here is where rights requests land — make sure someone reads it. ```text Section 14 theme={"system"} 14. Contact Us - Privacy Email: [privacy@yourcompany.com] - Support Email: [support@yourcompany.com] - Company Name: [full legal name] - Mailing Address: [postal address] - Business Hours: [e.g., Mon–Fri, 09:00–18:00 UTC+8] ``` Publish the privacy policy at a stable URL, then paste that URL into Section 11 of your [Terms of Service](/mor/account-reviews/tos) and into your checkout. Keep a version log with effective dates so you can prove what was live at any past point. # Terms of Service Template Source: https://docs.waffo.ai/mor/account-reviews/tos A drop-in T&C for Pancake merchants — covers Subscription, Credits, and Hybrid billing A complete, card-network-compliant Terms of Service for Pancake merchants. Pick your billing model, replace the bracketed placeholders, publish. This is a template, not legal advice. Have a lawyer review before publishing if your jurisdiction or business model is unusual. ## Pick your billing model Each model maps to its own billing-specific block (sections 4–7) further down. Pick the card that matches how you charge — it jumps straight to the matching block. Monthly or yearly recurring. Auto-renews until canceled. Buy credit packs and spend them. Optional auto top-up. A plan with credits **and** extra packs sold separately. ## Before you publish 🔴 items are what wins or loses disputes. 🟡 items materially strengthen your defense. * Legal name and address filled in (company: legal entity + registered address; individual / sole trader: legal name + business address) * Recurring / auto-top-up authorization in place (if you charge automatically) * In-product cancel flow exists (not email-only) * Refund policy filled in **and** shown at checkout * Third-party AI providers declared (if you use them) * Prohibited-uses list matches your actual product * Payment processor named: **`Waffo Pancake`** * Every contact channel listed is real and monitored * Billing descriptor set so customers recognize the name on their card statement — the #1 chargeback cause is "I don't recognize this charge" * Annual subscribers get a renewal reminder \~7 days out * Free-trial users get a "trial converting" reminder \~7 days out * V2/V3 — credits-about-to-expire reminder * Every `[placeholder]` is replaced with real text * Version number and effective date filled in * V3 — picked one option from each of the three refund choices * Deleted the sections that don't apply (V1 users remove V2/V3 blocks, etc.) **On the refund policy display:** in Pancake, save the refund policy in your store settings — the checkout picks it up automatically. You don't need to keep two copies in sync.

Template

Copy the **common sections** below, then pick the **billing-specific block** that matches your model and slot it in between Section 3 and Section 8. Replace every `[placeholder]` with your real text. The template is in English. Translate or localize as your market requires.

Common — sections 1–3 and 8–16

```text Sections 1–3 & 8–16 (use in every version) theme={"system"} [LEGAL NAME] Terms of Service — AI-Powered Services 1. Introduction & Acceptance of Terms Welcome to [Legal Name] ("[Legal Name]" / "we" / "us" / "our"). [Pick the line that fits you and delete the other:] [If a company:] a company incorporated under the laws of [Country/Region] with its registered address at [Legal Registered Address]. [If an individual / sole trader:] an individual / sole trader operating under the laws of [Country/Region] with a business address at [Business Address]. These Terms of Service ("Terms") govern your access to and use of our AI-powered services, including [Brief service description], available at [Website URL] (the "Service"). By creating an account, subscribing to any plan, purchasing credits, or otherwise using the Service, you confirm that you: (a) are at least 18 years of age; (b) have read, understood, and agree to be bound by these Terms; (c) agree to our Privacy Policy; and (d) are authorized to enter into this agreement on behalf of yourself or any organization you represent. ⚠️ Important: If you do not agree to these Terms, you may not use the Service. Continued use after any update constitutes acceptance of the revised Terms. 2. Service Description 2.1 What We Provide [Legal Name] provides an AI-powered platform that enables users to [describe core functionality]. The Service is a digital software-as-a-service (SaaS) product delivered via the internet. 2.2 Nature of the Service The Service is a digital, intangible product. Upon confirmed payment, access is granted immediately. Due to the immediate digital delivery, specific limitations apply to refunds as described in Section 7. 2.3 AI Model Dependency (optional) [Optional: Our Service utilizes third-party AI model providers including [e.g., OpenAI, Anthropic, Google]. Service performance and availability may be affected by the operational status of these providers. We will endeavor to communicate significant disruptions in a timely manner.] 3. Account Registration & Eligibility 3.1 Account Creation To use the Service, you must create an account with accurate and complete information. You are responsible for maintaining the confidentiality of your login credentials and for all activities under your account. 3.2 Account Security - Notify us immediately of any unauthorized use at [security@company.com]. - Do not share your credentials with any third party. - Keep your billing email address up to date. 3.3 Business Accounts If you register on behalf of a company or organization, you represent that you have authority to bind that entity to these Terms. [INSERT SECTIONS 4–7 FROM THE V1 / V2 / V3 BLOCK MATCHING YOUR BILLING MODEL] 8. Billing Disputes If you believe there is an error in a charge, please contact us at [billing@company.com] before disputing with your bank. We commit to responding within 2 business days and resolving confirmed billing errors within 5 business days. 9. AI Output & Intellectual Property 9.1 Ownership of Outputs Subject to these Terms, AI-generated content produced in response to your inputs ("Outputs") is owned by you. You may use Outputs for any lawful purpose consistent with Section 10. 9.2 Input License By submitting content to the Service ("Inputs"), you grant [Legal Name] a limited, non-exclusive license to process your Inputs solely to deliver the Service. We do not use your Inputs to train AI models without your explicit consent. 9.3 Third-party AI Model Dependency (if applicable) [Include if applicable] Our Service uses third-party AI model providers including [e.g., OpenAI, Anthropic, Google]. Outputs may be influenced by these providers' models. We do not guarantee specific output quality and accept no liability for outputs generated by third-party models. 9.4 Accuracy of AI Outputs AI-generated content may contain errors or inaccuracies. You are solely responsible for reviewing and verifying Outputs before relying on them. 10. Acceptable Use Policy 10.1 Permitted Uses You may use the Service only for lawful purposes in accordance with these Terms. 10.2 Prohibited Uses You agree NOT to use the Service to: - Generate content that is illegal, defamatory, harassing, or fraudulent. - Generate, distribute, or facilitate deepfake content — including AI-synthesized audio, video, or images — that impersonates real individuals or is intended to deceive viewers about its origin or authenticity. - Generate content that sexualizes minors or violates child protection laws. - Produce malware, phishing content, or cyberattack tools. - Infringe the intellectual property or privacy rights of any third party. - Circumvent or interfere with security features of the Service. - Resell or sub-license access to the Service without prior written approval. - Violate any applicable law, regulation, or card network rule. 10.3 AI-Specific Prohibitions In addition to the general prohibitions above, given the AI-powered nature of the Service, you specifically agree NOT to: - Use Service inputs, outputs, model responses, or any derivative thereof to train, fine-tune, benchmark, distil, or otherwise develop any artificial intelligence or machine learning model that competes, directly or indirectly, with the Service or [Legal Name]'s products. - Systematically scrape, extract, or harvest model outputs at scale for purposes other than your own authorised use of the Service. - Represent AI-generated outputs as the work of a human professional (e.g., a licensed doctor, lawyer, or financial adviser) in contexts where such misrepresentation could cause harm. 11. Data, Privacy & Security Your use of the Service is governed by our Privacy Policy at [privacy policy URL]. Payment card data is processed exclusively by our PCI-DSS certified payment processor (Waffo Pancake) and is not stored on our servers. Account data is retained for [90] days following cancellation, then deleted. 12. Disclaimers & Limitation of Liability 12.1 Disclaimer of Warranties THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR ACCURACY OF AI OUTPUTS. 12.2 AI-Generated Content Disclaimer ⚠️ AI GENERATED CONTENT DISCLAIMER Outputs may be inaccurate, incomplete, or outdated. Do not rely on them for legal, medical, financial, or other professional advice. Always verify AI-generated content with a qualified professional before acting on it. 12.3 Limitation of Liability TO THE MAXIMUM EXTENT PERMITTED BY LAW, [LEGAL NAME] SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES. OUR TOTAL LIABILITY SHALL NOT EXCEED THE AMOUNT YOU PAID IN THE [12] MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM. 13. Term & Termination These Terms remain effective while you use the Service. We may suspend or terminate your account if you materially breach these Terms, we suspect fraudulent activity, or as required by law. If we terminate for reasons other than your breach, we will provide a pro-rated refund for any unused prepaid period. You may delete your account at any time by contacting [support@company.com]. 14. Governing Law & Dispute Resolution These Terms are governed by the laws of [Country / State / Region]. Before initiating any formal proceedings, please contact us at [legal@company.com] to attempt informal resolution. [Optional: Any unresolved dispute shall be submitted to binding arbitration in [City] under the rules of [Arbitration Body].] 15. General Provisions We may update these Terms at any time. Material changes will be notified by email at least [14] days before the effective date. Continued use constitutes acceptance. If any provision is found unenforceable, the remainder continues in full effect. 16. Contact Information For any questions about these Terms or billing, please contact us: - General Support: [support@company.com] - Billing: [billing@company.com] - Refunds: [refunds@company.com] - Cancel Subscription: [cancel@company.com] or Account Settings - Legal & Privacy: [legal@company.com] - Security: [security@company.com] - Phone: [+X-XXX-XXX-XXXX] - Mailing Address: [Legal Name], [Street Address], [City, Country] By using the Service or checking "I agree" at checkout, you acknowledge and agree to these Terms. Last Updated: [DATE] · [Legal Name] · [Website URL] ```

Billing-specific — sections 4–7

Pick the one block that matches your billing model and slot it between Section 3 and Section 8. Delete the other two.

V1 — Subscription

```text V1 — Subscription (sections 4–7) theme={"system"} 4. Subscription Plans (V1) We offer subscription plans for access to the Service. [Describe subscription plans briefly, e.g.: We offer monthly and annual subscription plans.] For current plan details and pricing, please visit [pricing URL]. We reserve the right to modify plan features or introduce new plans at any time. Price changes are governed by Section 5.1. 5. Billing & Payment (V1) 5.1 Recurring Billing Authorization By providing your payment method and subscribing to a paid plan, you expressly authorize [Legal Name] to charge your payment method on a recurring basis for the applicable subscription fee. This recurring authorization remains in effect until you cancel in accordance with Section 6. Your subscription renews automatically at the end of each billing period unless cancelled. We will send you advance notice before the renewal of any annual subscription. 5.2 Taxes Prices are exclusive of applicable taxes (including VAT, GST, sales tax, or similar) unless otherwise stated. Where required by applicable law, we will collect and remit such taxes. 6. Cancellation Policy (V1) 6.1 How to Cancel You may cancel your subscription at any time through: - Online (recommended): Account Settings → Subscription → Cancel Subscription - Email: Send a cancellation request from your registered email to [cancel@company.com] Cancellation takes effect immediately. A confirmation email will be sent within [10] minutes. 6.2 Effect of Cancellation - Your subscription remains active until the end of the current billing period. - You will not be charged for subsequent billing periods. - Account data is retained for [90] days after cancellation, then deleted per our Privacy Policy. 7. Refund Policy (V1) 7.1 General Policy Due to the immediate and intangible nature of our digital services, subscription fees are generally non-refundable, except as expressly stated below. 7.2 Eligible Refunds - New subscriber 7-day guarantee: A full refund is available to first-time subscribers within 7 days of the initial charge, provided usage has not exceeded [X]% of the plan quota. - Duplicate charge: A full refund for any billing error resulting in a duplicate charge. - Verified service outage (≥72 hrs): A pro-rated credit or refund for extended outages within our control. - Statutory rights: If applicable law grants withdrawal or refund rights (e.g., 14-day withdrawal right in EU/UK), those rights are preserved. 7.3 How to Request a Refund To request a refund, contact us at [refunds@company.com] with your account email, transaction ID, and reason. We will acknowledge within 2 business days and process eligible refunds within 5–10 business days. 7.4 Non-Refundable Items - Fees for subscription periods already used (except as noted in 7.2). - Annual subscription fees requested after [30] days from initial purchase. - Accounts terminated for violation of these Terms or the Acceptable Use Policy. ```

V2 — Credits / Top-up

```text V2 — Credits / Top-up (sections 4–7) theme={"system"} 4. Credits & Top-up Plans (V2) The Service operates on a credit-based system. [Describe credit plans briefly, e.g.: Users may purchase credit packages at any time.] Credits are used to access Service features. For available credit packages and current pricing, please visit [pricing URL]. 5. Billing & Payment (V2) 5.1 Single Purchase Authorization Each credit purchase is a one-time transaction. By completing a purchase, you authorize [Legal Name] to charge your payment method for the amount displayed at checkout. 5.2 Auto Top-up Authorization (if enabled) [Optional — include ONLY if your product offers automatic top-up] If you choose to enable the Auto Top-up feature, you authorize [Legal Name] to automatically charge your saved payment method when your credit balance falls below the threshold you have set, at the amount you have configured in your Account Settings. You may disable Auto Top-up at any time. 5.3 Taxes Prices are exclusive of applicable taxes unless otherwise stated. Where required by law, applicable taxes will be collected at checkout. 6. Credits Validity & Account Termination (V2) 6.1 Credits Validity Purchased credits are valid for [X] months/years from the date of purchase, unless otherwise stated at the time of purchase. We will send you a reminder notification before your credits expire. 6.2 Termination of Account If your account is terminated (by you or by us), unused credits will be retained for [X] days following termination. After this period, unused credits expire without refund unless otherwise required by applicable law. 7. Refund Policy (V2) All credit purchases are generally non-refundable after credits have been consumed. The following exceptions apply: - Unused credits within 7 days of purchase: A full refund may be requested within 7 days of purchase if credits have not been used. - Duplicate charge: Full refund for any billing error. - Statutory rights (EU/UK/AU): 14-day withdrawal right for first-time purchasers is preserved, waived upon first use. To request a refund, contact [refunds@company.com] with your account email and transaction ID. ```

V3 — Hybrid

```text V3 — Hybrid (sections 4–7) theme={"system"} 4. Plans & Credits (V3) We offer both subscription plans and credit packages. [Describe subscription plans briefly.] [Describe credit packages briefly.] For current plan details and pricing, please visit [pricing URL]. 5. Billing & Payment (V3) 5.1 Subscription Recurring Billing Authorization By subscribing to a paid plan, you expressly authorize [Legal Name] to charge your payment method on a recurring basis for the applicable subscription fee until you cancel per Section 6. 5.2 Credit Purchase Authorization Each credit purchase is a one-time transaction authorized at checkout. 5.3 Auto Top-up Authorization (if enabled) [Optional — include ONLY if your product offers auto top-up] If you enable Auto Top-up, you authorize [Legal Name] to automatically charge your payment method when your credit balance falls below your configured threshold, at the amount configured in your Account Settings. You may disable this at any time. 5.4 Taxes Prices are exclusive of applicable taxes unless otherwise stated. 6. Cancellation Policy (V3) You may cancel your subscription at any time via Account Settings → Subscription → Cancel, or by emailing [cancel@company.com]. Cancellation takes effect at the end of the current billing period. Credits purchased separately are not cancelled and remain subject to their validity period. 7. Refund Policy (V3) 7.1 Subscription Refunds - 7-day guarantee for first-time subscribers: Full refund if usage has not exceeded [X]% of plan quota. - Duplicate charge or service failure (≥72 hrs): Eligible for refund or credit. 7.2 Credits Refunds - Unused credits within 7 days of purchase: Eligible for full refund. - Used credits are non-refundable. 7.3 Subscription Plan Credits — Rollover Policy (pick one, delete the other) [Option A1 — No rollover] Credits included in a subscription plan expire at the end of each billing period and do not carry over to the next period. [Option A2 — Rollover] Credits included in a subscription plan accumulate across billing periods and do not expire as long as your subscription remains active (subject to a maximum balance limit specified in your plan). 7.4 Credits After Subscription Cancellation (pick one, delete the others) [Option B1] Separately purchased credits retain their original validity period regardless of subscription status. [Option B2] After subscription cancellation, separately purchased credits remain usable for [X] days. [Option B3] After subscription cancellation, separately purchased credits become immediately unavailable; a refund will be issued for any unused balance in accordance with Section 7.2. 7.5 Credit Consumption Order (pick one, delete the other) [Option C1] When both subscription plan credits and separately purchased credits are available, subscription plan credits are consumed first. [Option C2] When both subscription plan credits and separately purchased credits are available, separately purchased credits are consumed first. ``` ## Section-by-section reference Each section shows its **tier** (🔴 load-bearing · 🟡 strongly recommended · ⚪ optional), a one-line summary, and the matching **template snippet**. Sections 4–7 vary by billing model — pick the one block (V1, V2, or V3) that matches how you charge.

1\. Introduction & Acceptance

**🔴 load-bearing** Names the seller — a company gives its legal entity + registered address; a sole trader / individual gives their legal name + business address. A brand name alone won't do. Fill `[Legal Name]`, the address line that fits you, `[Country/Region]`, `[Brief service description]`, `[Website URL]`. ```text Section 1 (common) theme={"system"} [LEGAL NAME] Terms of Service — AI-Powered Services 1. Introduction & Acceptance of Terms Welcome to [Legal Name] ("[Legal Name]" / "we" / "us" / "our"). [Pick the line that fits you and delete the other:] [If a company:] a company incorporated under the laws of [Country/Region] with its registered address at [Legal Registered Address]. [If an individual / sole trader:] an individual / sole trader operating under the laws of [Country/Region] with a business address at [Business Address]. These Terms of Service ("Terms") govern your access to and use of our AI-powered services, including [Brief service description], available at [Website URL] (the "Service"). By creating an account, subscribing to any plan, purchasing credits, or otherwise using the Service, you confirm that you: (a) are at least 18 years of age; (b) have read, understood, and agree to be bound by these Terms; (c) agree to our Privacy Policy; and (d) are authorized to enter into this agreement on behalf of yourself or any organization you represent. ⚠️ Important: If you do not agree to these Terms, you may not use the Service. Continued use after any update constitutes acceptance of the revised Terms. ```

2\. Service Description

**🔴 load-bearing** "Digital SaaS, delivered immediately on payment" — the line that pushes back when customers claim non-delivery. Fill `[Legal Name]`, `[describe core functionality]`. Keep 2.3 only if you depend on third-party AI providers. ```text Section 2 (common) theme={"system"} 2. Service Description 2.1 What We Provide [Legal Name] provides an AI-powered platform that enables users to [describe core functionality]. The Service is a digital software-as-a-service (SaaS) product delivered via the internet. 2.2 Nature of the Service The Service is a digital, intangible product. Upon confirmed payment, access is granted immediately. Due to the immediate digital delivery, specific limitations apply to refunds as described in Section 7. 2.3 AI Model Dependency (optional) [Optional: Our Service utilizes third-party AI model providers including [e.g., OpenAI, Anthropic, Google]. Service performance and availability may be affected by the operational status of these providers. We will endeavor to communicate significant disruptions in a timely manner.] ```

3\. Account Registration & Eligibility

**🔴 load-bearing** Establishes that the account holder is responsible — blocks "someone else used my login." Fill `[security@company.com]`. ```text Section 3 (common) theme={"system"} 3. Account Registration & Eligibility 3.1 Account Creation To use the Service, you must create an account with accurate and complete information. You are responsible for maintaining the confidentiality of your login credentials and for all activities under your account. 3.2 Account Security - Notify us immediately of any unauthorized use at [security@company.com]. - Do not share your credentials with any third party. - Keep your billing email address up to date. 3.3 Business Accounts If you register on behalf of a company or organization, you represent that you have authority to bind that entity to these Terms. ```

4–7. Billing block — V1 Subscription

**🔴 load-bearing** Plans, recurring authorization, in-product cancel, and refund policy for recurring subscriptions. Section 5.1 is the clause card networks care about most — the customer must **explicitly** authorize recurring charges. Don't go with "no refunds"; at minimum give first-time buyers 7 days. Prices stay on your pricing page, not here. ```text V1 — Subscription (sections 4–7) theme={"system"} 4. Subscription Plans (V1) We offer subscription plans for access to the Service. [Describe subscription plans briefly, e.g.: We offer monthly and annual subscription plans.] For current plan details and pricing, please visit [pricing URL]. We reserve the right to modify plan features or introduce new plans at any time. Price changes are governed by Section 5.1. 5. Billing & Payment (V1) 5.1 Recurring Billing Authorization By providing your payment method and subscribing to a paid plan, you expressly authorize [Legal Name] to charge your payment method on a recurring basis for the applicable subscription fee. This recurring authorization remains in effect until you cancel in accordance with Section 6. Your subscription renews automatically at the end of each billing period unless cancelled. We will send you advance notice before the renewal of any annual subscription. 5.2 Taxes Prices are exclusive of applicable taxes (including VAT, GST, sales tax, or similar) unless otherwise stated. Where required by applicable law, we will collect and remit such taxes. 6. Cancellation Policy (V1) 6.1 How to Cancel You may cancel your subscription at any time through: - Online (recommended): Account Settings → Subscription → Cancel Subscription - Email: Send a cancellation request from your registered email to [cancel@company.com] Cancellation takes effect immediately. A confirmation email will be sent within [10] minutes. 6.2 Effect of Cancellation - Your subscription remains active until the end of the current billing period. - You will not be charged for subsequent billing periods. - Account data is retained for [90] days after cancellation, then deleted per our Privacy Policy. 7. Refund Policy (V1) 7.1 General Policy Due to the immediate and intangible nature of our digital services, subscription fees are generally non-refundable, except as expressly stated below. 7.2 Eligible Refunds - New subscriber 7-day guarantee: A full refund is available to first-time subscribers within 7 days of the initial charge, provided usage has not exceeded [X]% of the plan quota. - Duplicate charge: A full refund for any billing error resulting in a duplicate charge. - Verified service outage (≥72 hrs): A pro-rated credit or refund for extended outages within our control. - Statutory rights: If applicable law grants withdrawal or refund rights (e.g., 14-day withdrawal right in EU/UK), those rights are preserved. 7.3 How to Request a Refund To request a refund, contact us at [refunds@company.com] with your account email, transaction ID, and reason. We will acknowledge within 2 business days and process eligible refunds within 5–10 business days. 7.4 Non-Refundable Items - Fees for subscription periods already used (except as noted in 7.2). - Annual subscription fees requested after [30] days from initial purchase. - Accounts terminated for violation of these Terms or the Acceptable Use Policy. ```

4–7. Billing block — V2 Credits / Top-up

**🔴 load-bearing** Credit packs, one-time purchase authorization, validity, and refunds. Include 5.2 only if you offer auto top-up. State explicitly that consumed credits are non-refundable; allow a 7-day refund on untouched credits. ```text V2 — Credits / Top-up (sections 4–7) theme={"system"} 4. Credits & Top-up Plans (V2) The Service operates on a credit-based system. [Describe credit plans briefly, e.g.: Users may purchase credit packages at any time.] Credits are used to access Service features. For available credit packages and current pricing, please visit [pricing URL]. 5. Billing & Payment (V2) 5.1 Single Purchase Authorization Each credit purchase is a one-time transaction. By completing a purchase, you authorize [Legal Name] to charge your payment method for the amount displayed at checkout. 5.2 Auto Top-up Authorization (if enabled) [Optional — include ONLY if your product offers automatic top-up] If you choose to enable the Auto Top-up feature, you authorize [Legal Name] to automatically charge your saved payment method when your credit balance falls below the threshold you have set, at the amount you have configured in your Account Settings. You may disable Auto Top-up at any time. 5.3 Taxes Prices are exclusive of applicable taxes unless otherwise stated. Where required by law, applicable taxes will be collected at checkout. 6. Credits Validity & Account Termination (V2) 6.1 Credits Validity Purchased credits are valid for [X] months/years from the date of purchase, unless otherwise stated at the time of purchase. We will send you a reminder notification before your credits expire. 6.2 Termination of Account If your account is terminated (by you or by us), unused credits will be retained for [X] days following termination. After this period, unused credits expire without refund unless otherwise required by applicable law. 7. Refund Policy (V2) All credit purchases are generally non-refundable after credits have been consumed. The following exceptions apply: - Unused credits within 7 days of purchase: A full refund may be requested within 7 days of purchase if credits have not been used. - Duplicate charge: Full refund for any billing error. - Statutory rights (EU/UK/AU): 14-day withdrawal right for first-time purchasers is preserved, waived upon first use. To request a refund, contact [refunds@company.com] with your account email and transaction ID. ```

4–7. Billing block — V3 Hybrid

**🔴 load-bearing** Subscription **plus** separately-sold credits. Combines recurring and one-time authorization. Refund section 7.3/7.4/7.5 has three picks (rollover, post-cancellation validity, consumption order) — choose one option each and delete the rest. Explain your choice on your help center too. ```text V3 — Hybrid (sections 4–7) theme={"system"} 4. Plans & Credits (V3) We offer both subscription plans and credit packages. [Describe subscription plans briefly.] [Describe credit packages briefly.] For current plan details and pricing, please visit [pricing URL]. 5. Billing & Payment (V3) 5.1 Subscription Recurring Billing Authorization By subscribing to a paid plan, you expressly authorize [Legal Name] to charge your payment method on a recurring basis for the applicable subscription fee until you cancel per Section 6. 5.2 Credit Purchase Authorization Each credit purchase is a one-time transaction authorized at checkout. 5.3 Auto Top-up Authorization (if enabled) [Optional — include ONLY if your product offers auto top-up] If you enable Auto Top-up, you authorize [Legal Name] to automatically charge your payment method when your credit balance falls below your configured threshold, at the amount configured in your Account Settings. You may disable this at any time. 5.4 Taxes Prices are exclusive of applicable taxes unless otherwise stated. 6. Cancellation Policy (V3) You may cancel your subscription at any time via Account Settings → Subscription → Cancel, or by emailing [cancel@company.com]. Cancellation takes effect at the end of the current billing period. Credits purchased separately are not cancelled and remain subject to their validity period. 7. Refund Policy (V3) 7.1 Subscription Refunds - 7-day guarantee for first-time subscribers: Full refund if usage has not exceeded [X]% of plan quota. - Duplicate charge or service failure (≥72 hrs): Eligible for refund or credit. 7.2 Credits Refunds - Unused credits within 7 days of purchase: Eligible for full refund. - Used credits are non-refundable. 7.3 Subscription Plan Credits — Rollover Policy (pick one, delete the other) [Option A1 — No rollover] Credits included in a subscription plan expire at the end of each billing period and do not carry over to the next period. [Option A2 — Rollover] Credits included in a subscription plan accumulate across billing periods and do not expire as long as your subscription remains active (subject to a maximum balance limit specified in your plan). 7.4 Credits After Subscription Cancellation (pick one, delete the others) [Option B1] Separately purchased credits retain their original validity period regardless of subscription status. [Option B2] After subscription cancellation, separately purchased credits remain usable for [X] days. [Option B3] After subscription cancellation, separately purchased credits become immediately unavailable; a refund will be issued for any unused balance in accordance with Section 7.2. 7.5 Credit Consumption Order (pick one, delete the other) [Option C1] When both subscription plan credits and separately purchased credits are available, subscription plan credits are consumed first. [Option C2] When both subscription plan credits and separately purchased credits are available, separately purchased credits are consumed first. ```

8\. Billing Disputes

**🔴 load-bearing** Where customers reach you before going to their bank. Fill `[billing@company.com]`. ```text Section 8 (common) theme={"system"} 8. Billing Disputes If you believe there is an error in a charge, please contact us at [billing@company.com] before disputing with your bank. We commit to responding within 2 business days and resolving confirmed billing errors within 5 business days. ```

9\. AI Output & Intellectual Property (if you ship AI)

**🔴 load-bearing** Customer owns outputs; you have a limited license to process inputs; you don't train on inputs without consent. Fill `[Legal Name]` in 9.2; add 9.3 if you use OpenAI / Anthropic / Google or similar. ```text Section 9 (common) theme={"system"} 9. AI Output & Intellectual Property 9.1 Ownership of Outputs Subject to these Terms, AI-generated content produced in response to your inputs ("Outputs") is owned by you. You may use Outputs for any lawful purpose consistent with Section 10. 9.2 Input License By submitting content to the Service ("Inputs"), you grant [Legal Name] a limited, non-exclusive license to process your Inputs solely to deliver the Service. We do not use your Inputs to train AI models without your explicit consent. 9.3 Third-party AI Model Dependency (if applicable) [Include if applicable] Our Service uses third-party AI model providers including [e.g., OpenAI, Anthropic, Google]. Outputs may be influenced by these providers' models. We do not guarantee specific output quality and accept no liability for outputs generated by third-party models. 9.4 Accuracy of AI Outputs AI-generated content may contain errors or inaccuracies. You are solely responsible for reviewing and verifying Outputs before relying on them. ```

10\. Acceptable Use Policy

**🔴 load-bearing** What customers can't do, including AI-specific prohibitions (no competing models, no deepfakes, no impersonating professionals). No placeholders — read the list and make sure it covers your product. For a fuller content policy, pair this with the [AUP template](/mor/account-reviews/aup). ```text Section 10 (common) theme={"system"} 10. Acceptable Use Policy 10.1 Permitted Uses You may use the Service only for lawful purposes in accordance with these Terms. 10.2 Prohibited Uses You agree NOT to use the Service to: - Generate content that is illegal, defamatory, harassing, or fraudulent. - Generate, distribute, or facilitate deepfake content — including AI-synthesized audio, video, or images — that impersonates real individuals or is intended to deceive viewers about its origin or authenticity. - Generate content that sexualizes minors or violates child protection laws. - Produce malware, phishing content, or cyberattack tools. - Infringe the intellectual property or privacy rights of any third party. - Circumvent or interfere with security features of the Service. - Resell or sub-license access to the Service without prior written approval. - Violate any applicable law, regulation, or card network rule. 10.3 AI-Specific Prohibitions In addition to the general prohibitions above, given the AI-powered nature of the Service, you specifically agree NOT to: - Use Service inputs, outputs, model responses, or any derivative thereof to train, fine-tune, benchmark, distil, or otherwise develop any artificial intelligence or machine learning model that competes, directly or indirectly, with the Service or [Legal Name]'s products. - Systematically scrape, extract, or harvest model outputs at scale for purposes other than your own authorised use of the Service. - Represent AI-generated outputs as the work of a human professional (e.g., a licensed doctor, lawyer, or financial adviser) in contexts where such misrepresentation could cause harm. ```

11\. Data, Privacy & Security

**🔴 load-bearing** PCI-DSS handling plus a privacy-policy link. **Pancake merchants: name `Waffo Pancake` as the processor.** Fill `[privacy policy URL]` (point it at your published [privacy policy](/mor/account-reviews/privacy-policy)) and `[90] days`. ```text Section 11 (common) theme={"system"} 11. Data, Privacy & Security Your use of the Service is governed by our Privacy Policy at [privacy policy URL]. Payment card data is processed exclusively by our PCI-DSS certified payment processor (Waffo Pancake) and is not stored on our servers. Account data is retained for [90] days following cancellation, then deleted. ```

12\. Disclaimers & Limitation of Liability

**🔴 load-bearing** As-is disclaimer + AI-output disclaimer + a 12-month liability cap. Fill `[LEGAL NAME]` (all caps) and `[12] months`. Keep §12.2 — it's the clause that protects you when customers act on AI output and lose money. ```text Section 12 (common) theme={"system"} 12. Disclaimers & Limitation of Liability 12.1 Disclaimer of Warranties THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR ACCURACY OF AI OUTPUTS. 12.2 AI-Generated Content Disclaimer ⚠️ AI GENERATED CONTENT DISCLAIMER Outputs may be inaccurate, incomplete, or outdated. Do not rely on them for legal, medical, financial, or other professional advice. Always verify AI-generated content with a qualified professional before acting on it. 12.3 Limitation of Liability TO THE MAXIMUM EXTENT PERMITTED BY LAW, [LEGAL NAME] SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES. OUR TOTAL LIABILITY SHALL NOT EXCEED THE AMOUNT YOU PAID IN THE [12] MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM. ```

13\. Term & Termination

**🔴 load-bearing** When you can suspend, and how customers delete their account. Fill `[support@company.com]`. ```text Section 13 (common) theme={"system"} 13. Term & Termination These Terms remain effective while you use the Service. We may suspend or terminate your account if you materially breach these Terms, we suspect fraudulent activity, or as required by law. If we terminate for reasons other than your breach, we will provide a pro-rated refund for any unused prepaid period. You may delete your account at any time by contacting [support@company.com]. ```

14\. Governing Law & Dispute Resolution

**⚪ optional** Jurisdiction + an optional arbitration clause. Fill `[Country / State / Region]` and `[legal@company.com]`. The arbitration sentence is optional — confirm with a lawyer if unsure. ```text Section 14 (common) theme={"system"} 14. Governing Law & Dispute Resolution These Terms are governed by the laws of [Country / State / Region]. Before initiating any formal proceedings, please contact us at [legal@company.com] to attempt informal resolution. [Optional: Any unresolved dispute shall be submitted to binding arbitration in [City] under the rules of [Arbitration Body].] ```

15\. General Provisions

**🔴 load-bearing** Boilerplate with a 14-day minimum notice for terms changes. Fill `[14] days` (30 is friendlier). ```text Section 15 (common) theme={"system"} 15. General Provisions We may update these Terms at any time. Material changes will be notified by email at least [14] days before the effective date. Continued use constitutes acceptance. If any provision is found unenforceable, the remainder continues in full effect. ```

16\. Contact Information

**🔴 load-bearing** Six channels by default. Better to list fewer real ones than six unmonitored — every channel here is checked during dispute handling. If you can only commit to one reliably, use it for every row. ```text Section 16 (common) theme={"system"} 16. Contact Information For any questions about these Terms or billing, please contact us: - General Support: [support@company.com] - Billing: [billing@company.com] - Refunds: [refunds@company.com] - Cancel Subscription: [cancel@company.com] or Account Settings - Legal & Privacy: [legal@company.com] - Security: [security@company.com] - Phone: [+X-XXX-XXX-XXXX] - Mailing Address: [Legal Name], [Street Address], [City, Country] By using the Service or checking "I agree" at checkout, you acknowledge and agree to these Terms. Last Updated: [DATE] · [Legal Name] · [Website URL] ``` After publishing, store the T\&C URL, version number, and effective date together. When you change terms, notify existing users at least 14 days in advance — and keep a version log so you can prove what was active at any past point in time. # Fees Source: https://docs.waffo.ai/mor/fees 3.9% + $0.50 per successful transaction. No monthly fees. No setup costs. Pay only when you make sales. *** ## Fee Schedule ### Successful Transactions The rate depends on the payment method: | Payment method | Percentage | Fixed | | ----------------------- | ---------- | ---------------- | | Cards & digital wallets | 3.9% | \$0.50 | | WeChat Pay | 3.9% | — (no fixed fee) | **Example (card):** ``` Sale (tax inclusive): $100 Fee: $100 × 3.9% + $0.50 = $4.40 You receive: $95.60 ``` **Example (WeChat Pay):** ``` Sale (tax inclusive): $100 Fee: $100 × 3.9% = $3.90 You receive: $96.10 ``` *** ### Failed Transactions Failed transaction fees apply only to specific failure types. They are not cumulative. | Failure Type | Fee | Notes | | ---------------------------------------------- | ---------------- | --------------------------------------------- | | Failed authentication (3DS) | \$0.30 / attempt | Card declined at the 3DS authentication stage | | Authentication passed but authorization failed | \$0.30 / attempt | Cleared 3DS but declined at authorization | | Did not reach authentication | \$0 | 3DS was never initiated — no fee charged | Failed transaction fees are charged per attempt, not per checkout session. Sessions that never trigger 3DS authentication are free. *** ### Refunds | Item | Fee | | ------------------------ | ----------------- | | Refund processing | \$1.00 per refund | | Original transaction fee | Not returned | The \$1.00 refund fee is charged per refund request. The original 3.9% + \$0.50 transaction fee is not refunded regardless of whether the refund is full or partial. *** ### Payouts | Item | Fee | | ----------- | -------------------------- | | Payout fee | 1% of payout amount | | Minimum fee | \$10.00 per payout request | Payout fees are charged per payout request, not per transaction. The minimum \$10.00 applies even if 1% of the payout amount is less than \$10. **Example:** ``` Payout: $500 Fee: $500 × 1% = $5.00 → minimum applies → $10.00 Pancake sends: $490.00 Payout: $2,000 Fee: $2,000 × 1% = $20.00 Pancake sends: $1,980.00 ``` The amount that actually lands in your account is determined by your receiving bank. Intermediary bank fees, FX conversion, and bank posting rules can create differences. *** ### Chargebacks | Event | Fee | | ------------------------- | ------- | | First chargeback | \$25.00 | | Representment (re-filing) | \$10.00 | | Pre-arbitration | \$25.00 | The representment fee of \$10.00 is only charged if you opt in to contest the chargeback **and** we actually file on your behalf. *** ## What's Included All of the following are included at no additional charge: | Feature | Included | | ---------------------------- | -------- | | Payment processing | ✓ | | Global tax compliance | ✓ | | Tax calculation & collection | ✓ | | Tax remittance | ✓ | | All major card networks | ✓ | | Dashboard | ✓ | | Consumer Portal | ✓ | | Webhooks & API | ✓ | | Fraud protection | ✓ | *** ## Subscriptions with Trial Periods For 7-day trial subscriptions, the first billing cycle may result in a net loss of approximately \$0.50 — this covers the cost of a \$0 authorization check performed at trial start. *** ## vs Alternatives Competitor pricing is based on publicly available information and may change. FastSpring does not publish rates — pricing is negotiated directly. Always verify on their official pricing pages. | Provider | Transaction Fee | Monthly | | ----------------- | ----------------------------- | ------- | | **Waffo Pancake** | 3.9% + \$0.50 | \$0 | | Paddle | 5% + \$0.50 | \$0 | | Lemon Squeezy | 5% + \$0.50 | \$0 | | Gumroad | \~10% | \$0 | | FastSpring | Not published (contact sales) | \$0 | *** ## FAQ No monthly minimum. You only pay fees when transactions occur. Once 3DS authentication is triggered, \$0.30 / attempt applies — whether authentication itself fails or the card clears 3DS and then fails at authorization. Sessions that never reach authentication (customer leaves before 3DS) are free. No. The 3.9% + \$0.50 fee is non-refundable. An additional \$1.00 refund processing fee applies per refund request. Same 3.9% + \$0.50 per successful renewal. No extra subscription management charges. # Prohibited & Restricted Products Source: https://docs.waffo.ai/mor/prohibited-products Categories that require additional review or are not permitted on Waffo Pancake. As a Merchant of Record, Waffo Pancake is responsible for every transaction processed through our platform. To maintain compliance with payment networks, financial regulations, and applicable laws, certain product and service categories are either restricted (accepted only under specific conditions) or prohibited entirely. Selling prohibited products will result in immediate account suspension and potential fund withholding. If you are unsure whether your product qualifies, contact support before listing. *** ## Restricted Categories These categories are accepted on Waffo Pancake, but require additional review and must meet specific conditions before you can begin selling. Contact our team to discuss your use case. | Category | Conditions & Notes | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **Skill Games** | Must be skill-based with no element of chance; prizes must be disclosed clearly; age verification required where applicable | | **Streaming Services** | Content must not include unauthorized copyrighted material; must comply with digital rights management obligations | | **AIGC (AI-Generated Content)** | Content must not infringe third-party IP; must include clear disclosure that content is AI-generated | | **Online Educational Platforms** | Credentials and qualifications claimed must be accurate; refund policies must be clearly stated | | **Luxury Items** | Authenticity must be verifiable; products must be accompanied by proper provenance documentation | | **Food Products** | Must comply with applicable food safety regulations in all markets sold; proper labeling required | | **Alcohol** | Must meet local licensing and age verification requirements in each jurisdiction; delivery restrictions apply | | **In-Game Virtual Items** | Must not facilitate unauthorized secondary markets; must comply with game publisher terms | | **VPN / VPS Services** | Must not be marketed for bypassing legal restrictions; acceptable use policies must be published | | **IPTV** | Only licensed content distribution is permitted; unauthorized rebroadcasting of copyrighted channels is prohibited | | **Subscription Services** | Cancellation terms must be clearly disclosed; auto-renewal must have explicit customer consent | | **Corrective Contact Lenses** | Valid prescription requirement must be enforced; must comply with medical device regulations in each market | | **Computer Software Stores** | All software sold must be properly licensed; no cracked, pirated, or key-reselling operations | | **Tele-Medicine** | Practitioners must be licensed in the jurisdictions they serve; cannot replace emergency medical services | | **Digital Games (Vietnam)** | Must hold valid licensing approval from Vietnamese authorities; subject to additional review for Vietnam sales | Approval for a restricted category does not guarantee ongoing eligibility. Waffo Pancake reserves the right to reassess accounts if business practices change or regulatory requirements are updated. *** ## Prohibited Categories The following categories are not permitted on Waffo Pancake under any circumstances. Products or services that are illegal, facilitate illegal activity, or promote harm: * Controlled substances, narcotics, and drug paraphernalia * Counterfeit identity documents, fake IDs, or fraudulent credentials * Telecommunications manipulation equipment (e.g., SIM cloners, call spoofing services) * Content that promotes, glorifies, or incites violence or hatred * Human trafficking or any services facilitating the exploitation of persons * Tobacco products and electronic cigarettes * Prescription drugs sold without a valid prescription process * Charities, political organizations, and religious organizations (payment collection via MoR is not appropriate for these entities) * Debt collection services Products or services that violate intellectual property rights: * Unauthorized distribution of copyrighted media (films, music, books, software) * Counterfeit goods — replicas or imitations misrepresenting brand or origin * Illegal imports or exports of goods prohibited from cross-border trade Products or services designed to deceive consumers or exploit participants: * Pyramid schemes, multi-level marketing structured around recruitment fees * Get-rich-quick schemes, guaranteed investment returns, or unrealistic income claims * Predatory lending products with deceptive terms * Services that sell or artificially inflate social media engagement (fake likes, followers, views, or reviews) * Pornographic or sexually explicit content of any kind * Prostitution, escort services, or any form of commercial sexual services * Firearms, handguns, rifles, and related accessories * Ammunition and explosive materials * Toxic, hazardous, radioactive, or biological materials * Any items regulated under arms control or export control laws * Online casinos, poker, and all other forms of gambling * Sports betting, sports forecasting competitions with cash prizes * Lotteries and sweepstakes where purchase is required for entry * Unregulated or unlicensed cryptocurrency exchanges * Initial Coin Offerings (ICOs) that do not meet applicable securities regulations * NFT marketplaces or projects where the primary use is speculative trading The following behaviors violate our platform policies and will result in immediate termination: * Creating accounts under a false identity or impersonating another individual or business * Processing payments on behalf of an undisclosed third-party merchant * Structuring transactions to avoid chargebacks, fraud detection, or reporting thresholds * Distributing malware, ransomware, spyware, or any malicious software through the platform *** ## Questions & Compliance This list is not exhaustive. Waffo Pancake may restrict additional categories based on evolving regulations, payment network requirements, or risk assessments. For the complete and legally binding terms, refer to the [Waffo Terms of Service](https://waffo.com/terms). Unsure if your product qualifies? Reach out before listing. Learn about our merchant verification process. # Supported Countries Source: https://docs.waffo.ai/mor/supported-countries What payment methods buyers can use, and how merchants receive payouts ## Sell Globally Waffo Pancake handles tax, compliance, and refunds as your Merchant of Record across most countries. The lists below describe **what's live today** — additional payment methods and payout corridors are on the roadmap. *** ## Buyer Payment Methods The following payment methods are live for buyers worldwide. Available options at checkout depend on the buyer's device, region, and currency. | Method | Notes | | ------------------------- | ------------------------------------------------------ | | Cards (Visa / Mastercard) | Available worldwide | | Apple Pay | iOS / macOS Safari, regions where Apple Pay is enabled | | Google Pay | Android / Chrome, regions where Google Pay is enabled | Need a regional method we don't list yet? Tell us which one and where you sell — submissions feed directly into our roadmap prioritization. *** ## Receive Payouts Waffo Pancake currently settles payouts in **CNY** via two channels — bank card and Alipay. More currencies are on the roadmap. Direct deposit to your bank card, settled in CNY. Fast transfers via Alipay, settled in CNY. Need payouts in a currency other than CNY? Contact support and let us know which corridor you need so we can prioritize it. *** ## Tax Compliance by Region We register and remit on behalf of merchants where required. Coverage depends on where you sell. ### United States * Sales tax in 45+ states * Nexus management * Automatic rate updates ### European Union * VAT registered * One-Stop Shop (OSS) * Digital services rules ### United Kingdom * UK VAT registered * Post-Brexit compliance ### Rest of World * Local compliance where required * Automatic threshold monitoring *** ## Unsupported Countries Due to international sanctions and compliance requirements, Waffo Pancake cannot process payments involving the following countries: | Code | Country | | ---- | -------------------------------- | | AFG | Afghanistan | | BDI | Burundi | | BLR | Belarus | | CAF | Central African Republic | | COD | Democratic Republic of the Congo | | CUB | Cuba | | GNB | Guinea-Bissau | | HTI | Haiti | | IRN | Iran | | IRQ | Iraq | | LBN | Lebanon | | LBY | Libya | | MLI | Mali | | MMR | Myanmar | | NIC | Nicaragua | | PRK | North Korea | | RUS | Russia | | SDN | Sudan | | SOM | Somalia | | SSD | South Sudan | | SYR | Syria | | YEM | Yemen | | ZWE | Zimbabwe | Transactions involving these countries — whether the consumer or merchant is located there — will be blocked. This list is subject to change based on evolving sanctions and compliance requirements. **Ukraine (Non-occupied Area)** is supported. Sanctions apply only to Russian-occupied territories within Ukraine, which are blocked at the territory level. *** ## Need Something Specific? Tell us which corridor or local payment method matters to you and we'll prioritize it. # What is MoR? Source: https://docs.waffo.ai/mor/what-is-mor We're the seller. You focus on building. ## Merchant of Record = We Handle Everything A **Merchant of Record (MoR)** is the legal entity that sells to your customers. When you use Waffo Pancake, we become your MoR. Legally, we sell to your customers. Revenue goes to you. Minus fees. *** ## How It Works ``` Customer pays → Waffo Pancake → We handle taxes → You receive payout ↓ We're the legal seller We issue invoices We remit taxes ``` *** ## Without MoR vs With MoR ### Without MoR (DIY) **You're responsible for:** * Registering for taxes in every country * Calculating correct tax rates * Filing tax returns everywhere * Staying compliant with changing regulations * Handling audits and disputes ### With MoR (Waffo Pancake) **We handle:** * Global tax registration * Tax calculation and collection * Tax remittance * Regulatory updates * Audits and disputes **You focus on:** Building your product. *** ## Global Tax Complexity | Country | Tax | Rate | Complexity | | --------- | --------- | ------ | ----------------------------- | | USA | Sales Tax | 0-10%+ | 10,000+ jurisdictions | | EU | VAT | 17-27% | 27 countries, different rules | | UK | VAT | 20% | Post-Brexit rules | | Canada | GST/HST | 5-15% | Federal + provincial | | Australia | GST | 10% | Digital services rules | Without an MoR, you'd need to register, track, file, and manage compliance in every jurisdiction you sell to. *** ## Real Cost Comparison | Approach | Monthly Cost | Time | | -------------------- | ---------------- | ---------------- | | DIY Global Tax | \$2,000-15,000+ | 20+ hours/month | | Hire Tax Consultants | \$2,000-10,000 | 5-10 hours/month | | **Waffo Pancake** | Included in fees | 0 hours | *** ## What We Handle Real-time calculation based on: * Customer location * Product type * Local tax rules * Exemptions and thresholds Correct tax at checkout: * Inclusive or exclusive pricing * Real-time rate updates We pay to authorities: * On-time filing * Correct jurisdictions * Proper documentation * All major card networks + digital wallets * Chargebacks and disputes * Refunds * Currency conversion *** ## MoR vs Payment Processor | Feature | Payment Processor | MoR | | ------------------ | ----------------- | --- | | Processes payments | Yes | Yes | | Tax calculation | No | Yes | | Tax collection | No | Yes | | Tax remittance | No | Yes | | Legal seller | You | MoR | | Tax liability | You | MoR | Using just Stripe? You're still responsible for all tax obligations. With us, we handle everything. *** ## Benefits ### Solo Founders Traditional MoRs require a legal entity. We don't. Get paid directly to your personal bank account. ### Startups Don't spend engineering time on billing. Ship features instead. ### Growing Companies Expand to new markets instantly. We're already compliant in 173 countries. *** ## FAQ Customers see your brand on checkout. Invoices show us as legal seller (required for tax compliance), but feature your branding prominently. No. MoR model requires us to process payments. This is how we legally handle your tax obligations. 173 countries. See the full list. Full list of countries where you can sell. # Quickstart Source: https://docs.waffo.ai/quickstart Ship your first payment in 5 minutes ## Choose Your Starting Point **Fastest to ship** Best for teams with an existing codebase that want Claude Code or another coding agent to plan the model, implement the integration, and drive validation. **Most control** Best when you need custom checkout flows, server-side orchestration, dynamic pricing, or tighter control over integration behavior. **Simplest** Best when you want to launch a payment link quickly first and deepen the integration later. *** ## Understand the Product Model In Waffo Pancake, a **product** is the billing object used for checkout and tax handling. It is not just a storefront card or SKU. | If you sell | Create in Waffo | Why | | ----------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------- | | Fixed-price download, template, license | **One-time product** | Customer pays once for a predefined price | | Monthly or annual plan | **Subscription product** | Customer is billed on a recurring schedule | | Starter / Pro / Enterprise plans | **Multiple subscription products** | Each plan option is its own billable product | | Usage overage, credits top-up, negotiated quote | **One-time checkout with `priceSnapshot`** | The final amount is calculated at checkout time, so pricing stays dynamic | If your business is subscription-based, you may still create **one-time charges** for setup fees, add-ons, overage billing, or credit packs. That is expected behavior. *** ## Integrate with AI ### 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. The skill file contains everything your AI needs — API endpoints, SDK patterns, webhook handling, and best practices. ### The Full Version You can also describe your stack for a tailored integration: ```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 → Merchant → API & Development 2. Create an API Key from Dashboard → Merchant → 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. My stack: [your stack — e.g., "Next.js with TypeScript"] ``` View the unified AI integration entry point for skill context, SDK patterns, code examples, catalog design, and dynamic pricing workflows. *** ## Ship Revenue in 5 Minutes No credit card required. No LLC needed. Just sign up and start selling. Get Started Steps *** ## 1. Create Account Visit [Merchant Dashboard](https://pancake.waffo.ai/merchant/auth/signin) and sign up with Google, GitHub, or Magic Link. Sign up with Google, GitHub, or Magic Link — no credit card required. Three options: * **Google OAuth** — One click. Done. * **GitHub OAuth** — For developers. Quick and easy. * **Magic Link** — Email. Click link. In. *** ## 2. Create Store First login triggers store setup: | Field | What It Does | | ---------- | --------------- | | Store Name | Your brand name | No business registration required. Solo founders welcome. *** ## 3. Create Your First Product Dashboard → Products → **Create Product** Create Product Form ```json theme={"system"} { "name": "Pro Monthly", "prices": { "USD": { "amount": "29.00", "taxIncluded": false, "taxCategory": "saas" } } } ``` For subscriptions, also set `billingPeriod` (`weekly`, `monthly`, `quarterly`, `yearly`). If your merchant account has multiple stores, confirm which store this product should belong to before creating it. If you offer multiple plans such as Starter / Pro / Enterprise: * Create **one subscription product per plan** * Start by defining the pricing and billing period for each plan clearly If you need dynamic pricing: * Create a base one-time product such as `Usage Overage` * Calculate the final amount on your server * Pass `priceSnapshot` when creating the checkout session Think of the product catalog as your billing model: fixed-price products for standard offers, subscription products for recurring access, and `priceSnapshot` for runtime pricing. *** ## 4. Integrate Payments Product created? You get a checkout link to add to your website: ``` https://checkout.waffo.ai/{store-slug}/{product-slug} ``` Add it to your website's product page. Done. This link is permanent — it never expires and stays valid even when you update the product. Just click "Copy Link" in the Dashboard. *** ## 5. Get Paid Customer pays → We handle taxes → You receive payout. Funds go directly to your bank account. No intermediary. No delays. Merchant Finance — balance and payouts *** ## Test vs Live * No real charges * Safe to experiment * Separate test data * Real payments * Real payouts * Toggle when ready **Switch in Dashboard header.** Toggle via `X-Environment` header in API. Always test first. *** ## Test Cards ### Successful Payments | Card | Type | | --------------------- | ----------------- | | `4576 7500 0000 0110` | Visa Credit | | `2226 9000 0000 0110` | Mastercard Credit | | `4001 7000 0000 0110` | Visa Debit | | `2226 9300 0000 0110` | Mastercard Debit | ### Declined Payments | Card | Type | | --------------------- | ----------------- | | `4576 7500 0000 0220` | Visa Credit | | `2226 9000 0000 0220` | Mastercard Credit | | `4001 7000 0000 0220` | Visa Debit | | `2226 9300 0000 0220` | Mastercard Debit | Any future expiry. Any CVC. *** ## Going Live Checklist Before flipping to Live Mode: * [ ] Test checkout flow end-to-end * [ ] Verify webhook endpoints (if using) * [ ] Add a payout account (Payout Accounts page) * [ ] Complete business details (Settings → Business Details) * [ ] Review product pricing * [ ] Sync products from test to production *** ## What's Next? Pricing models. Trials. Intervals. Recurring billing. Dunning. Lifecycle. Your brand. Your colors. Our infra. API keys. Webhooks. Your backend. *** ## Need Help? Community support. Fast answers. # Business Details Source: https://docs.waffo.ai/settings/business-details Complete the KYB review to enable production payments ## Why submit business details? As your Merchant of Record, Waffo Pancake needs a quick read on what you're selling so we can meet compliance requirements. Once reviewed, you unlock: * **Production payments** — accept real charges, not just test mode. * **Payouts** — funds settle into your bank account. * **Compliant invoices** — receipts carry the correct merchant information. You can build, test, and integrate everything in test mode without submitting business details. Submit only when you're ready to go live. *** ## Review flow In the dashboard, go to **Settings → Business Details** and complete the form. Once everything looks right, submit. We run an automated risk check and queue it for review. Review usually completes within 1–3 business days. Once approved, the store flips to production mode automatically and you can accept real payments. *** ## What you'll fill in ### Product description | Field | Required | Notes | | ------------------ | -------- | ---------------------------------------------------------------- | | About your product | Yes | A short description of what you sell. | | Product website | Yes | The public URL where buyers see your product. | | Contact email | Yes | Used for review communication and as your store's support email. | ### Seller type Pick whichever fits — both pass review on the same merit. | Type | Notes | | --------------------------- | ------------------------------------------------------ | | Individual / Solo developer | No company registration needed; operating as yourself. | | Registered business | A formally registered company or trade name. | Both paths are fully supported and reviewed on the same merits. Pick whichever matches your current setup — solo developers and registered businesses both pass review when the rest of the submission checks out. ### Product status | Field | Required | Notes | | ------------------ | -------- | ----------------------------------------------------- | | Product readiness | Yes | Where the product is today — building, launched, etc. | | Existing customers | Yes | Whether you already have paying users. | ### Compliance acknowledgements You confirm that: * Your product is not in a [prohibited category](/mor/prohibited-products). * You've read and agreed to the account review policy. * Your pricing page is publicly accessible. * Your product doesn't infringe trademarks. *** ## Review outcomes | Status | Meaning | | ------------- | ------------------------------------------------------------- | | **Pending** | Submitted, waiting in queue. | | **In review** | Reviewer is looking at your submission. | | **Approved** | Production mode is now enabled. | | **Returned** | Some information needs adjustment — resubmit when corrected. | | **Rejected** | Doesn't meet requirements — the rejection reason is included. | On approval, your contact email becomes the store's support email and your product website becomes the store URL — both are written back automatically. *** ## FAQ No — both individuals and registered businesses are supported. Pick whichever fits your current setup; the review applies the same criteria to both. 1–3 business days for typical submissions. More complex cases may take longer. Read the return reason, adjust the relevant fields, and resubmit. You can resubmit as many times as you need. Yes — edits after approval go through review again. Existing production functionality keeps working during the re-review. # Domain Verification Source: https://docs.waffo.ai/settings/domain-verification Prove ownership of your product website before submitting KYB ## Why we verify your domain Your website is the public face of your product. Before we approve KYB and enable production payments, we need to confirm that the domain you list as `Product website` actually belongs to you. Domain verification is a one-time step per store, and the binding stays in place after KYB approval. There are two entry points — both write to the same backend state, so verifying once is enough: * **Settings → General** — verify (or re-verify) your product domain at any time, independent of KYB. * **Settings → Business Details → Step 8 (Website URL)** — inline during KYB submission. The "Submit for review" button stays disabled until the domain on this step is verified. ## Verification methods Pancake offers four ways to verify a domain. Pick whichever your stack already supports — they are equivalent. | Method | Typical setup time | Best for | | ----------------------- | -------------------------- | -------------------------------------------------------------- | | Email domain auto-match | Instant | Sites where the support email already lives on the same domain | | DNS TXT record | 5–30 min (DNS propagation) | Most websites; works on any host | | HTML `` tag | 1–2 min (deploy required) | Single-page apps and statically generated sites | | `.well-known` file | 1–2 min (deploy required) | Anything that can serve a static file | Pick **one** method. Once any method succeeds, the others stop being relevant for this domain. ## Method 1 — Email domain auto-match (recommended) If your support email is already verified in **Step 7 (Contact Email)** and lives on the same domain as your website, Pancake automatically detects this and shows a one-click **Verify domain** button. **Example.** Support email `support@acme.com` (verified) + Product website `https://acme.com` → auto-match available. 1. Click **Verify domain**. 2. Pancake validates the email/domain pair on the server. Verification completes immediately. If the support email is on a different domain (e.g. `team@gmail.com` while the site is `acme.com`), this option is hidden — use one of the manual methods below. ## Method 2 — DNS TXT record The most reliable method. You add a TXT record to the DNS zone for your domain. 1. In the dashboard, click **Use another method** → **DNS TXT**. 2. Pancake shows three values: * **Type**: `TXT` * **Host / Name**: `_waffo-challenge` (the dashboard strips the `.your-domain.com` suffix automatically — most providers prepend it for you) * **Value**: a one-time challenge string starting with `waffo-domain-verify=…` 3. Open your DNS provider's dashboard and add the record exactly as shown. Leave TTL at the default. 4. Wait for propagation. Most providers publish within minutes; some (Cloudflare, Route 53) are nearly instant. 5. Back in Pancake, click **I've added the record**. We query the public DNS for the TXT value and complete verification on success. You can leave the TXT record in place after verification. It does no harm. If you remove it, the existing binding is unaffected — we only re-check at verification time. ### Provider-specific notes | Provider | Where to add TXT records | | -------------- | ----------------------------------------------------- | | Cloudflare | DNS → Records → Add record | | GoDaddy | My Products → DNS → Add → TXT | | Namecheap | Domain List → Manage → Advanced DNS → Add new record | | Route 53 | Hosted zones → your zone → Create record (Type = TXT) | | Google Domains | DNS → Custom records | If your provider expects a fully-qualified record name, use `_waffo-challenge.your-domain.com` instead of just `_waffo-challenge`. ## Method 3 — HTML `` tag Useful when you can't change DNS but you can deploy site code. 1. In the dashboard, click **Use another method** → **HTML meta tag**. 2. Pancake shows the snippet: ```html theme={"system"} ``` 3. Add it to the `` of your site's homepage (`https://your-domain.com/`). 4. Deploy. Verify the tag is present in the rendered HTML — view-source must include it; tags injected only after JS execution can fail if our crawler hits before hydration. If you're on a fully client-rendered SPA, prefer Method 2 or 4. 5. Back in Pancake, click **I've added the tag**. ## Method 4 — `.well-known` file Drop a static file under a fixed path. Works on any host that serves static assets. 1. In the dashboard, click **Use another method** → **.well-known file**. 2. Pancake shows: * **File URL**: `https://your-domain.com/.well-known/waffo-challenge.txt` * **File contents**: a single line starting with `waffo-domain-verify=…` 3. Create the file at exactly that path. Make sure the response is `200 OK`, `Content-Type: text/plain` (or any text-like type), and the body is the challenge value with no extra whitespace. 4. Confirm in a browser that the URL returns the expected content. 5. Back in Pancake, click **I've uploaded the file**. Common mistake: deploying the file at `/.well-known/waffo-challenge` (no extension) or returning a 301 redirect to `www.your-domain.com`. Both fail. Use the exact URL Pancake shows, served from the apex / canonical host you typed into the website field. ## Troubleshooting ### "Could not find the record / tag / file" Recheck the value. The challenge is generated per attempt; if you hit **Refresh** in Pancake, the previous value is invalidated. Make sure you copied the latest one. ### TXT record exists but verification still fails DNS caches. Try `dig TXT _waffo-challenge.your-domain.com` (Linux/macOS) or `nslookup -type=TXT _waffo-challenge.your-domain.com` (Windows). If the value isn't visible there yet, wait 5–10 minutes and retry. ### The challenge expired Each challenge has a short lifetime (the dashboard shows the exact time under the action button). If it expired, click **Refresh** in Pancake to issue a new one, then update your DNS / meta tag / file with the new value. ### I want to change my product website later Before KYB approval, you can revoke the current domain binding from **Step 8 (Website URL)** and re-verify a new one. After KYB approval the domain is locked — contact support to change it. ## Programmatic / API access If you're verifying domains as part of an automated onboarding flow, the underlying endpoints are: | Endpoint | Purpose | | ----------------------------------------------------------- | ------------------------------------------------ | | `POST /v1/actions/verification/start-domain-verification` | Issues a challenge for one of the four methods | | `POST /v1/actions/verification/confirm-domain-verification` | Asks the server to check the record / tag / file | Both require the merchant JWT (or API key) and the standard `X-Environment` header. ## Next steps After domain verification succeeds, head back to Business Details and complete the remaining steps (Step 9 onward). The Submit for review button unlocks once **all** steps including domain verification turn green. # Support Email Verification Source: https://docs.waffo.ai/settings/email-verification Verify the support email merchants can reach you on, and unlock one-click domain verification ## Why we verify your support email Your **support email** is what buyers see on receipts, in failed-payment notifications, and in the customer portal. Pancake verifies it before KYB approval to make sure the address is reachable and that you control the inbox. A verified support email also unlocks a fast-path for [domain verification](/settings/domain-verification): if your support email is on the same domain as your product website (for example, `support@example.com` ↔ `https://example.com`), Pancake offers a one-click **Verify domain** button — no DNS records or HTML files needed. ## Two entry points Both entry points write to the same backend state, so verifying once is enough: * **Settings → General** — verify (or change) your support email at any time, independent of KYB. * **Settings → Business Details → Step 7 (Contact Email)** — inline during KYB submission. The "Submit for review" button stays disabled until the support email on this step is verified. ## How to verify Use an inbox you control. Free providers (Gmail, Outlook, etc.) are accepted, but Pancake recommends an address on the same domain as your product website so you can also use the one-click domain fast-path. A 6-digit code is sent to the email. The button enters a 60-second cooldown before you can resend. The code stays valid for 10 minutes. If it expires, click resend and use the new one. The block flips to a green "Verified" state, and the support email is now bound to the store. You can change it later — re-verification is required after every change. ## Troubleshooting | Symptom | What to check | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Code never arrives | Check spam / promotions. Wait for the cooldown to end, then resend. | | "Invalid or expired code" | Codes expire 10 minutes after sending. Resend and use the new code. | | Verified email shows the wrong address | Re-enter the correct address and verify again — the latest verified address replaces the old one. | | Domain fast-path didn't appear after verifying | The domain fast-path only triggers when the email host matches the website domain exactly. Public providers (gmail.com, outlook.com) never trigger it. | ## What happens after verification * The support email appears on receipts, refund notifications, and the customer portal. * If it shares a domain with your product website, the [domain verification](/settings/domain-verification) page shows a one-click **Verify domain** option. * KYB review can proceed — Step 7 (Contact Email) of Business Details turns green. # Store Settings Source: https://docs.waffo.ai/settings/store-settings Configure your store profile, checkout appearance, notifications, and webhooks; create and delete stores ## Overview Store Settings lets you manage all store-level configurations. Access it from **Settings** in the sidebar. There are four tabs: | Tab | Description | | ----------------- | ---------------------------------------------- | | **General** | Store profile, branding, and verification | | **Checkout** | Checkout page appearance | | **Notifications** | Email notifications for you and your customers | | **Webhooks** | Webhook endpoints and event subscriptions | *** ## General ### Store Profile | Field | Description | | ----------------------- | ------------------------------------------------------------- | | **Store Name** | Your store's display name (also updates the public store URL) | | **Store Slug** | URL-friendly identifier, auto-generated from store name | | **Store Logo** | Square image, at least 200×200px | | **Support Email** | Customer-facing contact email shown on receipts | | **Website** | Your product website | | **Checkout Return URL** | Redirect URL after checkout success or failure | Changing the store name will also update the public store URL automatically. ### Store Review Complete store verification to enable live payments. Status options: | Status | Description | | ----------------------- | -------------------------------------------- | | **Not Started** | Verification not yet initiated | | **In Progress** | Partially filled, not yet submitted | | **Under Review** | Submitted, typically takes 1–3 business days | | **Verified** | Approved — live payments enabled | | **Verification Failed** | Not approved — update and resubmit | See [Business Details](/settings/business-details) for the full verification flow. ### Creating a New Store Each merchant account can have up to **20 stores**. Click your current store name in the top navigation bar and select **Create New Store**, then follow the setup flow. Each store is fully independent — with its own products, consumers, orders, and settings. ### Deleting a Store Permanently deletes the store and all associated data. This action cannot be undone. You must delete all products before deleting the store. *** ## Checkout Customize the appearance of your checkout page. Supports both light and dark themes. ### Branding * **Logo** — Shown in the checkout header. Synced with Store Logo in General Settings. ### Colors | Setting | Description | | -------------- | -------------------------- | | **Primary** | Button and highlight color | | **Background** | Page background | | **Card** | Card and panel backgrounds | | **Text** | Primary text color | ### Styling * **Border Radius** — Corner roundness for buttons, cards, and inputs ### Dual Theme (Light & Dark) Enable dual theme to configure separate color palettes for light and dark modes. Customers see the theme that matches their system preference, or you can set a default. Use **Import from website** to automatically extract your brand colors from your website URL. *** ## Notifications ### Customer Emails Configure which emails Waffo Pancake sends to your customers: | Email | Trigger | | ------------------------- | ------------------------------------------------------- | | Order confirmation | Customer completes a one-time purchase | | Subscription confirmation | Customer starts a new subscription | | Subscription cycled | Subscription automatically renews | | Subscription updated | Customer changes to a different subscription plan | | Subscription canceled | Customer cancels their subscription | | Subscription uncanceled | Customer reactivates a previously canceled subscription | | Subscription revoked | Canceled subscription permanently ends | | Subscription past due | Subscription payment fails | | Upcoming charge notice | Sent before next charge or trial ends | ### Your Notifications Configure which events send a notification to you: | Notification | Trigger | | --------------------- | ---------------------------------- | | New Orders | A new order is placed | | New Subscriptions | A new subscription starts | | Subscription Canceled | A subscription is canceled | | Refund Requests | A refund is requested or processed | | Chargebacks | A chargeback is received | | Payout Completed | A payout is completed | *** ## Webhooks Configure webhook endpoints to receive real-time event notifications. ### Endpoints You can configure separate endpoints for **Test** and **Live** environments. | Field | Description | | ---------------- | -------------------------------------------- | | **Endpoint URL** | HTTPS URL to receive webhook POST requests | | **Public Key** | Use to verify webhook signature authenticity | ### Event Types Select which events are delivered to each endpoint: | Group | Events | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Order** | `order.completed` | | **Subscription** | `subscription.activated`, `subscription.payment_succeeded`, `subscription.updated`, `subscription.canceling`, `subscription.uncanceled`, `subscription.canceled`, `subscription.past_due` | | **Refund** | `refund.succeeded`, `refund.failed` | ### Event Logs View recent webhook delivery history — status (succeeded / pending / failed), response body, and retry attempts. Use **Send test event** to trigger a test delivery. Failed deliveries are retried up to 3 times with exponential backoff. # Analytics Source: https://docs.waffo.ai/api-reference/endpoints/graphql/analytics GraphQL query examples for revenue, payment, and customer analytics ## Statistics Queries Aggregate statistics for your store's business metrics. ### Order Statistics ```graphql theme={"system"} query($storeId: String!) { orderStatistics(storeId: $storeId) { totalOrders totalRevenue } } ``` ### Payment Statistics ```graphql theme={"system"} query($storeId: String!) { paymentStatistics(storeId: $storeId) { totalPayments successRate } } ``` ### Product Statistics ```graphql theme={"system"} query($storeId: String!) { productStatistics(storeId: $storeId) { totalProducts activeProducts } } ``` ### Combined Dashboard Query Fetch all core metrics in a single request: ```graphql theme={"system"} query($storeId: String!) { orderStatistics(storeId: $storeId) { totalOrders totalRevenue } paymentStatistics(storeId: $storeId) { totalPayments successRate } productStatistics(storeId: $storeId) { totalProducts activeProducts } } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx" } ``` ### SDK Example ```typescript theme={"system"} const result = await client.graphql.query<{ orderStatistics: { totalOrders: number; totalRevenue: number }; paymentStatistics: { totalPayments: number; successRate: number }; productStatistics: { totalProducts: number; activeProducts: number }; }>({ query: `query($storeId: String!) { orderStatistics(storeId: $storeId) { totalOrders totalRevenue } paymentStatistics(storeId: $storeId) { totalPayments successRate } productStatistics(storeId: $storeId) { totalProducts activeProducts } }`, variables: { storeId: "STO_3bVzrkD0FJjFdZNLk8Ualx" }, }); ``` *** ## Trend Analysis Track metrics over time periods: ```graphql theme={"system"} query($storeId: String!) { trendAnalysis(storeId: $storeId) { period orders revenue payments } } ``` *** ## Distribution Analysis Analyze revenue distribution by currency, country, or product: ```graphql theme={"system"} query($storeId: String!) { distributionAnalysis(storeId: $storeId) { dimension value count amount } } ``` *** ## Customer Analysis Understand your customer base: ```graphql theme={"system"} query($storeId: String!) { customerAnalysis(storeId: $storeId) { totalCustomers newCustomers returningCustomers } } ``` *** ## Subscription Analysis Track subscription health metrics: ```graphql theme={"system"} query($storeId: String!) { subscriptionAnalysis(storeId: $storeId) { activeSubscriptions churnRate mrr } } ``` *** ## Tax Analysis Review tax collection by region: ```graphql theme={"system"} query($storeId: String!) { taxAnalysis(storeId: $storeId) { country taxAmount orderCount } } ``` *** ## Refund Ticket Analysis Track refund patterns: ```graphql theme={"system"} query($storeId: String!) { refundTicketAnalysis(storeId: $storeId) { totalTickets approvedCount rejectedCount totalRefundedAmount } } ``` # Orders & Payments Source: https://docs.waffo.ai/api-reference/endpoints/graphql/orders-and-payments GraphQL query examples for orders, payments, and refund tickets ## One-Time Orders ### List Completed Orders ```graphql theme={"system"} query($storeId: String!) { onetimeOrders( storeId: $storeId filter: { status: { eq: "paid" } } limit: 50 ) { id buyerEmail currency priceSnapshot { currency subtotal taxAmount total taxCategory } status createdAt } onetimeOrdersCount(storeId: $storeId) } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx" } ``` ### Filter by Date Range ```graphql theme={"system"} query($storeId: String!) { onetimeOrders( storeId: $storeId filter: { createdAt: { gte: "2026-01-01T00:00:00.000Z" lte: "2026-03-31T23:59:59.999Z" } } ) { id buyerEmail status createdAt } } ``` ### Look Up One-Time Order by Your Business Number (`orderMerchantExternalId`) Pass the same `orderMerchantExternalId` you attached at checkout creation. ```graphql theme={"system"} query($storeId: String!, $ref: String!) { onetimeOrders( storeId: $storeId filter: { orderMerchantExternalId: { eq: $ref } } ) { id buyerEmail status orderMerchantExternalId createdAt } } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx", "ref": "ORDER-2026-00891" } ``` ### SDK Example ```typescript theme={"system"} const result = await client.graphql.query<{ onetimeOrders: Array<{ id: string; buyerEmail: string; priceSnapshot: { total: number; currency: string }; status: string; }>; }>({ query: `query($storeId: String!) { onetimeOrders(storeId: $storeId, filter: { status: { eq: "paid" } }) { id buyerEmail priceSnapshot { total currency } status } }`, variables: { storeId: "STO_3bVzrkD0FJjFdZNLk8Ualx" }, }); ``` *** ## Subscription Orders ### List Active Subscriptions ```graphql theme={"system"} query($storeId: String!) { subscriptionOrders( storeId: $storeId filter: { status: { in: ["active", "trialing"] } } ) { id buyerEmail status billingPeriod createdAt } subscriptionOrdersCount(storeId: $storeId) } ``` ### Filter Canceling Subscriptions ```graphql theme={"system"} query($storeId: String!) { subscriptionOrders( storeId: $storeId filter: { status: { eq: "canceling" } } ) { id buyerEmail status createdAt } } ``` ### Look Up Subscription Order by Your Business Number (`orderMerchantExternalId`) Pass the same `orderMerchantExternalId` you attached at checkout creation. Every renewal payment that follows inherits the same value, so a single business reference resolves the whole subscription lifecycle. ```graphql theme={"system"} query($storeId: String!, $ref: String!) { subscriptionOrders( storeId: $storeId filter: { orderMerchantExternalId: { eq: $ref } } ) { id buyerEmail status billingPeriod orderMerchantExternalId createdAt } } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx", "ref": "SUB-2026-01045" } ``` *** ## Payments ### List Successful Payments ```graphql theme={"system"} query { payments( filter: { status: { eq: "succeeded" } } limit: 100 ) { id orderId status refundStatus snapshotAmountDetails { subtotal taxAmount total currency } createdAt } paymentsCount(filter: { status: { eq: "succeeded" } }) } ``` ### Filter by Date Range ```graphql theme={"system"} query { payments( filter: { status: { eq: "succeeded" } createdAt: { gte: "2026-01-01T00:00:00.000Z" lte: "2026-03-31T23:59:59.999Z" } } ) { id orderId snapshotAmountDetails { subtotal taxAmount total currency } createdAt } } ``` ### Look Up Payment by Waffo Payment ID ```graphql theme={"system"} query($paymentId: String!) { payment(id: $paymentId) { id orderId status refundStatus orderMerchantExternalId snapshotAmountDetails { total currency } } } ``` **Variables:** ```json theme={"system"} { "paymentId": "PAY_6eYCunG3IMmIgcQOnaXdoA" } ``` ### Look Up Payment by Your Business Number (`orderMerchantExternalId`) Pass the same `orderMerchantExternalId` you attached at checkout creation. Field name mirrors webhook payload `data.orderMerchantExternalId`. ```graphql theme={"system"} query($ref: String!) { payments(filter: { orderMerchantExternalId: { eq: $ref } }) { id orderId status refundStatus orderMerchantExternalId createdAt } } ``` **Variables:** ```json theme={"system"} { "ref": "ORDER-2026-00891" } ``` For subscription orders, every renewal payment **inherits the same `orderMerchantExternalId`** set at checkout. The query above returns the full payment history for the same business reference, ordered by `createdAt DESC`. *** ## Refunds (executed records) Refund records (`order.refunds`) are written after the PSP confirms the refund. They are **separate from refund tickets** — tickets carry the request lifecycle, refund records carry the executed result. ### Look Up Refund by Waffo Refund ID `Refund` exposes both business numbers as flat fields (`orderMerchantExternalId` from the originating order, `refundTicketMerchantExternalId` from the originating refund ticket) — same naming as the webhook payload. ```graphql theme={"system"} query($refundId: String!) { refund(id: $refundId) { id paymentId ticketId status orderMerchantExternalId refundTicketMerchantExternalId pspAmountDetails { amount currency } createdAt } } ``` **Variables:** ```json theme={"system"} { "refundId": "RFD_8aHbCcDdEeFfGgHhIiJjKk" } ``` ### Look Up Refunds by Payment (Waffo Payment ID) ```graphql theme={"system"} query($paymentId: String!) { refunds(filter: { paymentId: { eq: $paymentId } }) { id status orderMerchantExternalId refundTicketMerchantExternalId pspAmountDetails { amount currency } createdAt } } ``` ### Look Up Refunds by Your Business Number Match by **the refund ticket's business reference** (what you attached when creating the refund ticket) — use the `refundTicketMerchantExternalId` filter: ```graphql theme={"system"} query($ref: String!) { refunds(filter: { refundTicketMerchantExternalId: { eq: $ref } }) { id paymentId ticketId status orderMerchantExternalId refundTicketMerchantExternalId pspAmountDetails { amount currency } createdAt } } ``` Or match by **the order's business reference** (returns all refunds against any payment carrying that reference, useful for subscription renewals) — filter at the payment layer and read its nested refunds: ```graphql theme={"system"} query($paymentRef: String!) { payments(filter: { orderMerchantExternalId: { eq: $paymentRef } }) { id orderMerchantExternalId refunds { id status refundTicketMerchantExternalId pspAmountDetails { amount currency } createdAt } } } ``` *** ## Refund Tickets ### List All Refund Tickets ```graphql theme={"system"} query { refundTickets { id paymentId status requestedAmount reason createdAt } refundTicketsCount } ``` ### Filter Pending Refunds ```graphql theme={"system"} query { refundTickets(filter: { status: { eq: "pending" } }) { id paymentId requestedAmount reason createdAt } } ``` ### Refunds for a Specific Payment ```graphql theme={"system"} query($paymentId: String!) { refundTickets(filter: { subjectId: { eq: $paymentId } }) { id status refundTicketMerchantExternalId versionData { reason requestedAmount { amount currency } } createdAt } } ``` **Variables:** ```json theme={"system"} { "paymentId": "PAY_6eYCunG3IMmIgcQOnaXdoA" } ``` ### Look Up Refund Tickets by Your Business Number (`refundTicketMerchantExternalId`) ```graphql theme={"system"} query($ref: String!) { refundTickets(filter: { refundTicketMerchantExternalId: { eq: $ref } }) { id status refundTicketMerchantExternalId versionData { reason requestedAmount { amount currency } } reviewNote rejectReason executedAt createdAt } } ``` **Variables:** ```json theme={"system"} { "ref": "REF-2026-00891" } ``` `RefundTicket` (the request) and `Refund` (the executed record) are **two different GraphQL types**, but their business-side identifiers share the same flat names as the webhook payload: `refundTicketMerchantExternalId` on both `RefundTicket` and `Refund`; `orderMerchantExternalId` is additionally exposed on `Refund` (inherited from the originating order). Webhook payloads carry the same two values as `data.orderMerchantExternalId` and `data.refundTicketMerchantExternalId`. *** ## Checkout Sessions ### Query Session Details ```graphql theme={"system"} query($sessionId: ID!) { checkoutSession(id: $sessionId) { id productType currency status expiresAt createdAt } } ``` **Variables:** ```json theme={"system"} { "sessionId": "cs_550e8400-e29b-41d4-a716-446655440000" } ``` # Stores & Products Source: https://docs.waffo.ai/api-reference/endpoints/graphql/stores-and-products GraphQL query examples for stores, products, and product versions ## Stores ### List All Stores ```graphql theme={"system"} query { stores { id name status slug supportEmail website prodEnabled createdAt updatedAt } } ``` ### Single Store with Nested Products ```graphql theme={"system"} query($storeId: ID!) { store(id: $storeId) { id name status onetimeProducts { id name prices status } subscriptionProducts { id name billingPeriod prices status } } } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx" } ``` *** ## One-Time Products ### List Products with Filtering ```graphql theme={"system"} query($storeId: String!) { onetimeProducts( filter: { storeId: { eq: $storeId } status: { eq: "active" } } limit: 20 offset: 0 ) { id name description prices status version media successUrl metadata createdAt updatedAt } onetimeProductsCount( filter: { storeId: { eq: $storeId } } ) } ``` **Variables:** ```json theme={"system"} { "storeId": "STO_3bVzrkD0FJjFdZNLk8Ualx" } ``` ### SDK Example ```typescript theme={"system"} const result = await client.graphql.query<{ onetimeProducts: Array<{ id: string; name: string; prices: Record; status: string; }>; onetimeProductsCount: number; }>({ query: `query($storeId: String!) { onetimeProducts(filter: { storeId: { eq: $storeId }, status: { eq: "active" } }, limit: 20) { id name prices status } onetimeProductsCount(filter: { storeId: { eq: $storeId } }) }`, variables: { storeId: "STO_3bVzrkD0FJjFdZNLk8Ualx" }, }); ``` *** ## Subscription Products ### List Subscription Products ```graphql theme={"system"} query($storeId: String!) { subscriptionProducts(filter: { storeId: { eq: $storeId } }) { id name description billingPeriod prices status version media metadata createdAt updatedAt } subscriptionProductsCount(filter: { storeId: { eq: $storeId } }) } ``` *** ## Product Versions ### Query Version History Each product update creates an immutable version. Query version history to see all past configurations: ```graphql theme={"system"} query($productId: String!) { onetimeProductVersions(filter: { productId: { eq: $productId } }) { id versionNumber name description prices media createdAt } } ``` ### Subscription Product Versions ```graphql theme={"system"} query($productId: String!) { subscriptionProductVersions(filter: { productId: { eq: $productId } }) { id versionNumber name billingPeriod prices metadata createdAt } } ``` **Variables:** ```json theme={"system"} { "productId": "PROD_4cWAslE1GKkGeaOMl9Vbmy" } ``` *** ## Merchants ### List Merchants ```graphql theme={"system"} query { merchants { id email status createdAt } merchantsCount } ``` ### Single Merchant ```graphql theme={"system"} query($id: ID!) { merchant(id: $id) { id email status createdAt updatedAt } } ``` # Billing Questions Source: https://docs.waffo.ai/customers/billing Understanding charges on your statement ## Why "Waffo Pancake" on My Statement? Waffo Pancake is a **Merchant of Record**. We process payments for many businesses. When you buy from a business using us, you may see: * "WAFFO" or "WAFFO PANCAKE" on your bank statement * Our name on invoices **This is normal.** We process payments on behalf of the business you purchased from. *** ## Invoices Invoices are automatically included in your order confirmation email after each purchase. ### View or Download Invoice Log in to the [Consumer Portal](https://pancake.waffo.ai/consumer/portal/login), open the order in **Payment History**, and click **View Invoice**. View Invoice in Consumer Portal Check the order confirmation email sent to your inbox. The invoice is attached or linked. You can also request an invoice directly from the business. ### Update Invoice Details Need to add or change billing information on your invoice? Click **Edit Details** on the invoice page, fill in your company name, Tax ID, and other details, then save. Edit Invoice Details ### Can't Download? If you're unable to download your invoice: * Email us at `support@waffo.ai` with your order details * Or click **Report Issue** in the [Consumer Portal](https://pancake.waffo.ai/consumer/portal/login) *** ## Refunds If there was a problem with an item you purchased, please first try contacting the original merchant. You can find the merchant's contact email at the bottom of your receipt email. If the issue persists and you can't resolve it with the seller, you can submit a refund request through the [Consumer Portal](https://pancake.waffo.ai/consumer/portal/login). ### How to Request In the Consumer Portal, find the order in **Payment History**, click **Report Issue**, select a category, describe the issue, and submit. Request refund via Report Issue ### Refund Policy * Refund requests must be submitted within **120 days** of product delivery * Refunds may be approved in the following cases: | Condition | Description | | ------------------------ | ----------------------------------------------------------- | | Defective product | Product is defective or does not function as described | | Unauthorized transaction | Transaction was unauthorized or made in genuine error | | Technical issues | Unable to access product due to technical issues on our end | | Legal requirement | Other situations required by law | * Once approved, refunds are returned to your original payment method within **14 business days** * Once you begin downloading or using digital content, the 14-day cancellation period is waived * Unused subscription periods are not refundable Refunds appear as separate credit, not reversal of original charge. If you disagree with a refund decision, email `support@waffo.ai` to appeal. # For Customers Source: https://docs.waffo.ai/customers/overview Bought something? We're here to help. ## Customer Portal Made a purchase from a business using Waffo Pancake? Find answers here. Log in to view orders, manage subscriptions, and download invoices. ### How to Log In We use **Magic Link** login — no password needed. Go to [Consumer Portal](https://pancake.waffo.ai/consumer/portal/login) and enter the email address you used when making your purchase. We'll send an email with a login link. Click the link to log in. ### Can't Log In? 1. Make sure you're using the exact email address from your purchase (check your receipt) 2. Check your spam folder for emails from `auth@waffo.ai` 3. Go back to the login page and request a new Magic Link Email `support@waffo.ai` with your purchase email and order details, and we'll help you out. *** ## Common Topics Charges, refunds, invoices, and taxes. View or cancel subscriptions. *** ## Contact Product or service usage and delivery issues. Find their contact in your receipt email. Unrecognized charges, payment processing, or billing issues. Email [support@waffo.ai](mailto:support@waffo.ai). # Manage Subscription Source: https://docs.waffo.ai/customers/subscriptions View or cancel anytime ## Access Consumer Portal Manage subscriptions through the [**Consumer Portal**](https://pancake.waffo.ai/consumer/portal/login). Self-service. No waiting. Go to [Consumer Portal](https://pancake.waffo.ai/consumer/portal/login) and log in. View all your active subscriptions under **Subscriptions**. View or cancel. *** ## View Subscription Details Click any subscription under **Subscriptions** in the Consumer Portal to view: | Info | Description | | ------------------- | ----------------------------------------------------------- | | Status | Subscription status (Active, Trialing, Canceling, Canceled) | | Start Date | When the subscription started | | Renewal Date | Next renewal date | | Order Details | Product name, price, and billing interval | | Payment Information | Payment method on file | | History | Past transaction records | View subscription details *** ## Subscription Billing ### When Subscriptions Bill | Interval | Billing Day | | --------- | ------------------------ | | Weekly | Same day each week | | Monthly | Same date each month | | Quarterly | Same date every 3 months | | Yearly | Same date each year | ### Price Changes Businesses must notify you: * Immediately for decreases * 30+ days for increases *** ## Cancel Subscription 1. Open portal link 2. Find subscription 3. Click **Cancel Subscription** 4. Confirm Cancel Subscription A confirmation dialog will appear. Click **Confirm Cancellation** to complete. Confirm Cancellation Please submit your cancellation request at least **1 hour** before the end of the current billing period. Cancellation takes effect on the next payment date. ### After Cancellation Cancellation takes effect on the next payment date. You can continue using the service until the end of the current paid period. If you change your mind, you can click **Resume Subscription** in the subscription details before the End Date to restore it. Resume Subscription *** ## Renewal ### Automatic Renewal Paid subscriptions renew automatically until you cancel. You'll receive: * Email reminder before renewal (1 day for weekly, 3 days for monthly/quarterly/yearly) * Receipt after each charge If the subscription price increases, we will notify you and seek your consent where required. ### Stop Renewal Submit your cancellation request at least **1 hour** before the end of the current billing period: * Current period: Access continues until period ends * Next period: No charge, access ends *** ## FAQ Canceling stops future charges, but unused subscription periods are not refundable. See [Refund Policy](/customers/refunds) for details. Yes. No cancellation fees unless specified by the business. Before the subscription's End Date, you can click **Resume Subscription** in the subscription details to restore it. Resume Subscription Possible reasons: * Cancellation set for "end of period" * Multiple subscriptions * Cancellation didn't complete — contact us *** ## Contact Product or service usage and delivery issues. Find their contact in your receipt email. Unrecognized charges, payment processing, or billing issues. Email [support@waffo.ai](mailto:support@waffo.ai).