Skip to main content
Documentation menu

Waffo Pancake

How to set up and use Waffo Pancake for payments and subscriptions

MinuteShip uses Waffo Pancake for payment processing, supporting both one-time payments and subscriptions. Waffo Pancake is a Merchant of Record (MoR): it acts as the legal seller and handles global tax calculation, collection, compliance, and payouts on your behalf.

The integration uses the official @waffo/pancake-ts SDK with Waffo's hosted checkout and consumer portal, so no card data ever touches your server.

Setup

MinuteShip template provides three pricing plans by default: a free plan, a pro subscription plan (monthly/yearly), and a lifetime plan (one-time payment). Follow these steps to set up:

Create Waffo Account

Sign up for a Waffo Pancake account at pancake.waffo.ai and create a store during onboarding. You can find more about the basics in the Waffo Quickstart.

Get API Keys

Get your Merchant ID and private key from the Waffo Dashboard:

  • Go to Waffo Dashboard > API & Development and click Create API Key to generate a new key pair
  • Copy the Merchant ID (starts with MER_) and the private key
  • Save them to your environment file as WAFFO_MERCHANT_ID and WAFFO_PRIVATE_KEY

Note

WAFFO_MERCHANT_ID is the Merchant ID — not a storeId and not a store identifier from a URL. API keys are bound to either test or production at creation time, so create a separate key for each environment.

The private key is server-only. When storing a PEM key in an environment variable, preserve the line breaks with escaped \n:

WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"

You can find more about API Key authentication in the Waffo documentation.

Set Up Webhook

Set up a webhook and subscribe to the payment events:

  • Go to Waffo Dashboard > Settings > Webhooks and click Add Webhook
  • Choose the Raw payload format (only Raw carries the X-Waffo-Signature header used for verification)
  • Enter Webhook URL: https://YOUR-DOMAIN.com/api/webhooks/waffo
  • All events are subscribed by default. Keep at least the following:
    • order.completed
    • subscription.activated
    • subscription.payment_succeeded
    • subscription.renewed
    • subscription.recovered
    • subscription.plan_changed
    • subscription.canceling
    • subscription.uncanceled
    • subscription.canceled
    • subscription.past_due
    • refund.succeeded
    • refund.failed

Note

Unlike Stripe and Creem, there is no webhook signing secret environment variable for Waffo. The @waffo/pancake-ts SDK embeds the verification public keys, and the template passes the expected test or production environment to verifyWebhook().

You can find more about webhooks and webhook signature verification in the Waffo documentation.

Create Products and Pricing Plans

Create products in Waffo and set up pricing plans. Waffo uses Product IDs (starts with PROD_) for checkout, not price IDs:

  • Go to Waffo Dashboard > Products and click Create Product
  • Create the Pro subscription plan products:
    • Product type: Subscription
    • Name: Pro Plan
    • Billing period: Monthly — save and copy the Product ID for VITE_WAFFO_PRODUCT_PRO_MONTHLY
    • Create a second subscription product with billing period Yearly — save and copy the Product ID for VITE_WAFFO_PRODUCT_PRO_YEARLY
  • Create the Lifetime plan product:
    • Product type: One-time
    • Name: Lifetime Plan
    • Save and copy the Product ID for VITE_WAFFO_PRODUCT_LIFETIME

Note

Create separate subscription products for each billing period (monthly/yearly), as recommended in the Waffo subscriptions guide. New products start in test mode; publish them to production before you accept real payments — see Publish Product.

Add Environment Variables

Add the following environment variables:

.env

# Payment provider
VITE_PAYMENT_PROVIDER=waffo

# Waffo API credentials (server-side only)
WAFFO_MERCHANT_ID=MER_...
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"

# Optional: accept test-mode webhooks in a production build (smoke testing only)
# WAFFO_DEBUG=true

# Product IDs
VITE_WAFFO_PRODUCT_PRO_MONTHLY=PROD_...
VITE_WAFFO_PRODUCT_PRO_YEARLY=PROD_...
VITE_WAFFO_PRODUCT_LIFETIME=PROD_...

Update Website Configuration

Update the payment section in src/config/website.ts to configure pricing plans — amounts, currencies, intervals, and plan metadata. The enable, provider, and priceId fields are automatically resolved from your environment variables (VITE_PAYMENT_PROVIDER and VITE_WAFFO_PRODUCT_*), so you don't need to hardcode them.

You must configure this section to match the products you created in Waffo:

src/config/website.ts

payment: {
  enable: isPaymentEnabled,              // ← auto: true when VITE_PAYMENT_PROVIDER is set
  provider: isPaymentEnabled ? paymentProvider : undefined, // ← auto: 'waffo'
  price: {
    plans: {
      free: {
        id: 'free',
        prices: [],
        isFree: true,
        isLifetime: false,
      },
      pro: {
        id: 'pro',
        prices: [
          {
            type: 'subscription',
            priceId: priceIds.proMonthly,  // ← auto: from VITE_WAFFO_PRODUCT_PRO_MONTHLY
            amount: 990,                   // amount in cents ($9.90)
            currency: 'USD',
            interval: 'month',
          },
          {
            type: 'subscription',
            priceId: priceIds.proYearly,   // ← auto: from VITE_WAFFO_PRODUCT_PRO_YEARLY
            amount: 9900,                  // amount in cents ($99.00)
            currency: 'USD',
            interval: 'year',
          },
        ],
        isFree: false,
        isLifetime: false,
        popular: true,
      },
      lifetime: {
        id: 'lifetime',
        prices: [
          {
            type: 'one_time',
            priceId: priceIds.lifetime,     // ← auto: from VITE_WAFFO_PRODUCT_LIFETIME
            amount: 19900,                  // amount in cents ($199.00)
            currency: 'USD',
            allowPromotionCode: true,
          },
        ],
        isFree: false,
        isLifetime: true,
      },
    },
  },
},

Note

If you are setting up your environment, you can now go back to the Environment Configuration and continue. The rest of this document can be read later.


Core Features

  • One-time payment for lifetime membership
  • Recurring subscription payments (monthly/yearly)
  • Free trial period support
  • Hosted checkout with buyer attribution (authenticated checkout)
  • Webhook handling for payment, subscription, and refund events
  • Tax, compliance, and payouts handled by Waffo
  • Consumer portal with Magic Link login
  • Built-in pricing components (table, card, button)
  • Server-side actions for secure payment operations
  • Multiple pricing plan support (free, pro, lifetime)

Development

For local development, expose your server with an HTTPS tunnel so Waffo can reach your webhook endpoint. The official docs recommend ngrok; cloudflared also works. Do not use localtunnel — it strips custom HTTP headers such as X-Waffo-Signature:

ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000

Then:

  1. Set the tunnel URL (e.g. https://xxxx.ngrok-free.app/api/webhooks/waffo) as the test webhook URL in Waffo Dashboard > Settings > Webhooks. Never put a temporary tunnel URL into the production webhook.
  2. Make a test purchase on the website. After payment, click Done on Waffo's confirmation page.
  3. Verify that the browser returns through /settings/payment?checkout_id=..., waits for the matching webhook-backed payment row, and then redirects to /settings/billing.

The template also ships a Waffo sandbox E2E suite:

pnpm e2e:waffo

Note

Waffo provides a complete test environment with test cards — no real charges. In the hosted sandbox checkout you can pick Credit/Debit Card and use the Quick Fill Success option to complete a payment instantly. See Test Mode in the Waffo documentation.

Production

  1. Complete the store review / KYB so production payments are enabled — see Account Reviews
  2. Publish your subscription and one-time products from test to production. Publishing is a one-way, first-publish-only operation — see Publish Product
  3. Create a production API key and use it for WAFFO_MERCHANT_ID / WAFFO_PRIVATE_KEY
  4. Add the production webhook URL https://YOUR-DOMAIN.com/api/webhooks/waffo in Waffo Dashboard > Settings > Webhooks
  5. Leave WAFFO_DEBUG unset (or set to false) so test-mode events are rejected in production — a sandbox purchase must never grant real access

Checkout Confirmation

Waffo does not return its CHK_* Checkout Session ID in webhook payloads. Before checkout, MinuteShip generates a unique payment reference and sends it as orderMerchantExternalId. Waffo carries that reference into the webhook, and MinuteShip stores it in payment.sessionId.

The payment result page polls only the row for that reference and the signed-in user. An older paid order cannot complete a new checkout.

Customer Portal

Waffo provides a hosted consumer portal at https://pancake.waffo.ai/consumer/portal/login. Customers log in with the email used for their purchase and a one-time Magic Link — no password required. In the portal they can:

  • View active subscriptions, next billing date, and payment history
  • Cancel or reactivate subscriptions
  • Download PDF invoices and receipts
  • Update billing details
  • Request refunds

The Billing page (/settings/billing) sends customers to this portal via the "Manage subscription" button. You can find more about the Customer Portal in the Waffo documentation.

Webhook Events

Waffo supports the following webhook events:

EventDescription
order.completedOne-time order payment succeeded
subscription.activatedSubscription first payment succeeded
subscription.payment_succeededPayment receipt only; does not update subscription lifecycle state
subscription.renewedRenewal completed; advances the current period and keeps the subscription active
subscription.recoveredA previously failed subscription recovered and became active again
subscription.plan_changedSubscription product changed; the new Product metadata takes precedence over the original order metadata
subscription.cancelingCancellation requested — active until period ends
subscription.uncanceledCancellation withdrawn
subscription.canceledSubscription terminated (period ended)
subscription.past_dueRenewal payment failed, Waffo is retrying
refund.succeededRefund completed — access is revoked
refund.failedRefund failed

Note

Waffo delivers test and production events to the same endpoint, and every payload carries a mode field. The template rejects events whose mode does not match the running environment; in a production build, set WAFFO_DEBUG=true only when smoke-testing a deployed Worker with a sandbox merchant.

Webhook Responses and Retries

ResultHTTP status
Payment is disabled200
Waffo is not the configured payment provider404
Missing payload or X-Waffo-Signature header400
Invalid signature, malformed header, or stale timestamp400
Successfully processed or safely duplicated200
Temporary provider or database failure500

MinuteShip returns 500 for temporary failures so Waffo can retry the event. Handlers are idempotent, so repeated successful deliveries do not create another payment row.

Note

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

To test the Waffo integration, use Waffo's test mode with these test cards:

CardTypeResult
4576 7500 0000 0110Visa CreditSuccess
2226 9000 0000 0110Mastercard CreditSuccess
4576 7500 0000 0220Visa CreditDeclined

Any future expiry date and any 3-digit CVC work. You can find more about test mode and test cards in the Waffo documentation.

Best Practices

  1. Protect API keys: Never expose WAFFO_PRIVATE_KEY or the Merchant ID in client-side code
  2. Validate webhook signatures: Always verify the X-Waffo-Signature header (the SDK's verifyWebhook() does this)
  3. Match environments: Use the test or production API key and webhook URL that correspond to the environment you are running
  4. Handle errors gracefully: Provide user-friendly error messages when payments fail
  5. Test webhooks thoroughly: Complete a full purchase in test mode before going live

References