Waffo Pancake
How to set up and use Waffo Pancake for payments and subscriptions
MkSaaS supports Waffo Pancake for one-time payments and subscriptions. Waffo is a Merchant of Record (MoR), so it handles global tax calculation, collection, compliance, and payouts on your behalf.
The integration uses the official @waffo/pancake-ts SDK together with Waffo's hosted checkout and consumer portal. Card details do not pass through your application server.
Setup
MkSaaS includes a free plan, a Pro subscription plan with monthly and yearly intervals, and a Lifetime one-time plan by default.
Create a Waffo Account
Sign up at pancake.waffo.ai and create a store during onboarding. The Waffo Quickstart explains the basic account setup.
Get API Credentials
In Waffo Dashboard, open API & Development and click Create API Key.
Copy the Merchant ID and private key, then save them as
WAFFO_MERCHANT_ID and WAFFO_PRIVATE_KEY.
WAFFO_MERCHANT_ID is the Merchant ID, not a storeId or a store
identifier copied from a URL. API keys are bound to the test or
production environment when they are created, so create a separate key
for each environment.
The private key is server-only. Preserve PEM line breaks with escaped
\n characters when storing it in an environment variable:
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"Configure the Webhook
In Waffo Dashboard > Settings > Webhooks, click Add Webhook and
configure:
- Payload format: Raw. This format includes the
X-Waffo-Signatureheader required for verification. - URL:
https://YOUR-DOMAIN.com/api/webhooks/waffo - Events: keep the payment, subscription, and refund events listed in the Webhook Events section.
Waffo does not require a webhook secret environment variable. The
@waffo/pancake-ts SDK contains the verification keys and verifies the
raw request body with verifyWebhook().
Create Products
Waffo checkout uses Product IDs beginning with PROD_, rather than
Stripe-style price IDs:
- Create a monthly Subscription product and save its ID as
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY. - Create a yearly Subscription product and save its ID as
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY. - Create a One-time product and save its ID as
NEXT_PUBLIC_WAFFO_PRODUCT_LIFETIME. New products start in test mode. Publish them to production before accepting real payments; publishing is a one-way operation. See the Waffo subscriptions guide and Publish Product.
Add Environment Variables
Set the provider and Waffo credentials in .env:
NEXT_PUBLIC_PAYMENT_PROVIDER=waffo
# Server-side credentials
WAFFO_MERCHANT_ID=MER_...
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"
# Optional: accept sandbox webhooks in a production build for smoke tests
# WAFFO_DEBUG=true
# Product IDs
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY=PROD_...
NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY=PROD_...
NEXT_PUBLIC_WAFFO_PRODUCT_LIFETIME=PROD_...Configure Pricing
Keep the existing price.plans configuration in
src/config/website.tsx, but make its priceId values resolve to the
Waffo product IDs when Waffo is active. The amount, currency, interval,
and plan metadata must match the products configured in Waffo.
price: {
plans: {
pro: {
id: 'pro',
prices: [
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_WAFFO_PRODUCT_PRO_MONTHLY!,
amount: 990,
currency: 'USD',
interval: PlanIntervals.MONTH,
},
{
type: PaymentTypes.SUBSCRIPTION,
priceId: process.env.NEXT_PUBLIC_WAFFO_PRODUCT_PRO_YEARLY!,
amount: 9900,
currency: 'USD',
interval: PlanIntervals.YEAR,
},
],
isFree: false,
isLifetime: false,
},
},
},Core Features
- Monthly and yearly recurring subscriptions
- Lifetime one-time payments
- Hosted checkout without handling card data on your server
- Webhook handling for payments, subscriptions, and refunds
- Waffo-managed tax, compliance, and payouts
- Shared consumer portal with Magic Link login
- Provider-agnostic pricing, checkout, and billing components
Development
Waffo must be able to reach your webhook endpoint during local development. Expose the local server through an HTTPS tunnel:
ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000Register the tunnel URL as a test webhook, for example
https://xxxx.ngrok-free.app/api/webhooks/waffo. Do not put a temporary
tunnel URL in the production webhook. Avoid localtunnel because it can strip
the X-Waffo-Signature header.
The template includes a Waffo sandbox E2E suite:
pnpm e2e:waffoThe sandbox checkout does not create real charges. Choose Credit/Debit Card
and enter the sandbox card fields directly using one of the test cards
listed below. The automated suite avoids Quick Fill because it can leave
subscription confirmation pending. An ignored MkSaaS Template environment file can be reused by
setting WAFFO_ENV_FILE when running the local E2E helper.
After payment, click Done on Waffo's hosted confirmation page. The browser
returns through /payment?checkout_id=..., waits for the matching
webhook-backed payment row, and then redirects to Billing.
For automated sandbox setup, server ports, environment-file precedence, and database prerequisites, see Automated Testing and tests/e2e/waffo/README.md in the template.
Production
- Complete Waffo store review / KYB so production payments are enabled.
- Publish the subscription and one-time products from test to production.
- Create production API credentials and set
WAFFO_MERCHANT_IDandWAFFO_PRIVATE_KEYto those credentials. - Register
https://YOUR-DOMAIN.com/api/webhooks/waffoas the production webhook. - Leave
WAFFO_DEBUGunset or set it tofalse. Sandbox events must never grant access in a production build.
Customer Portal
Waffo provides the shared consumer portal at
https://pancake.waffo.ai/consumer/portal/login. Customers sign in with the
email used for their purchase and a one-time Magic Link. They can view
subscriptions and invoices, cancel or reactivate subscriptions, update billing
details, and request refunds.
Checkout Confirmation
Waffo does not return its CHK_* Checkout Session ID in webhook payloads.
Before checkout, MkSaaS generates a unique payment reference and sends it as
orderMerchantExternalId. Waffo carries that reference into the webhook, and
MkSaaS stores it in payment.sessionId.
The payment result page polls only the row for that reference and the signed-in user, so an older paid order cannot complete a new checkout.
Webhook Events
Configure the following events for /api/webhooks/waffo:
| Event | Description |
|---|---|
order.completed | One-time order payment succeeded |
subscription.activated | Subscription first payment succeeded |
subscription.payment_succeeded | Payment receipt only; does not update lifecycle state |
subscription.renewed | Renewal completed; advances the current period and keeps access active |
subscription.recovered | A previously failed subscription recovered and became active |
subscription.plan_changed | Subscription product changed; new product metadata takes precedence |
subscription.canceling | Cancellation requested; active until period end |
subscription.uncanceled | Cancellation withdrawn |
subscription.canceled | Subscription terminated |
subscription.past_due | Renewal payment failed and is being retried |
refund.succeeded | Refund completed and access is revoked |
refund.failed | Refund failed |
Waffo sends test and production events to the same endpoint and includes a
mode field in every payload. MkSaaS rejects events whose mode does not match
the running environment.
Webhook Responses and Retries
| Result | HTTP status |
|---|---|
| Waffo is not the configured provider | 404 |
Missing payload or X-Waffo-Signature header | 400 |
| Invalid signature, malformed header, or stale timestamp | 400 |
| Successfully processed or safely duplicated | 200 |
| Temporary provider or database failure | 500 |
Temporary failures return 500 so Waffo can retry. Handlers are idempotent,
so repeated successful deliveries do not create another payment row.
Any successful refund.succeeded event revokes access. Waffo does not expose
a reliable partial-versus-full refund marker, so a partial refund also revokes
access and must be handled manually if that is not your policy.
Test Cards
Use Waffo test mode with any future expiry date and any three-digit CVC:
| Card | Type | Result |
|---|---|---|
4576 7500 0000 0110 | Visa Credit | Success |
2226 9000 0000 0110 | Mastercard Credit | Success |
4576 7500 0000 0220 | Visa Credit | Declined |
Best Practices
- Keep
WAFFO_PRIVATE_KEYandWAFFO_MERCHANT_IDon the server only. - Use the Raw webhook format so
X-Waffo-Signatureis preserved. - Use test and production credentials in their matching environments.
- Complete a full sandbox checkout and webhook round trip before going live.
MkSaaS Docs