Skip to main content

Lemon Squeezy Failover Runbook

Switch from Stripe to Lemon Squeezy as the active payment provider in under 15 minutes.

Switch nSelf billing from Stripe to Lemon Squeezy in under 15 minutes. Existing Stripe subscriptions keep running — only new checkouts route through Lemon Squeezy.

When to use this

  • Stripe experiences a regional outage or checkout degradation
  • Stripe terminates the nSelf merchant account
  • EU VAT or US sales-tax compliance requires a Merchant of Record

Prerequisites

The following must be configured in ping_api environment variables before starting:

VariableWhere to find it
LEMONSQUEEZY_API_KEYLemon Squeezy dashboard → Settings → API Keys
LEMONSQUEEZY_STORE_IDDashboard URL: app.lemonsqueezy.com/stores/{id}
LEMONSQUEEZY_WEBHOOK_SECRETDashboard → Webhooks → your endpoint secret
LEMONSQUEEZY_VARIANT_ID_*Dashboard → Products → each product → Variants

Verify the webhook endpoint is registered at:

https://ping.nself.org/webhooks/lemonsqueezy

with all subscription events enabled (subscription_created, subscription_updated, subscription_cancelled, order_created).

Step-by-step activation (~10 min)

1. Verify LS credentials (2 min)

source ~/.claude/vault.env
curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $LEMONSQUEEZY_API_KEY" \
  https://api.lemonsqueezy.com/v1/stores/$LEMONSQUEEZY_STORE_ID
# Expected: 200

2. Health-check the admin endpoint (1 min)

curl -s -H "X-Admin-Key: $PING_API_ADMIN_KEY" \
  https://ping.nself.org/admin/payment-provider | jq .

Expected output:

{
  "active": "stripe",
  "persisted": "stripe",
  "stripe_configured": true,
  "lemonsqueezy_configured": true
}

If lemonsqueezy_configured is false, stop here — set the missing env vars and redeploy ping_api before continuing.

3. Switch the active provider (1 min)

curl -s -X POST \
  -H "X-Admin-Key: $PING_API_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"provider":"lemonsqueezy"}' \
  https://ping.nself.org/admin/payment-provider | jq .

Expected:

{
  "previous": "stripe",
  "active": "lemonsqueezy",
  "message": "Payment provider switched to lemonsqueezy. New checkouts will use lemonsqueezy. Existing subscriptions continue unaffected."
}

4. Verify the switch took effect (1 min)

curl -s -H "X-Admin-Key: $PING_API_ADMIN_KEY" \
  https://ping.nself.org/admin/payment-provider | jq .active
# Expected: "lemonsqueezy"

5. Test a checkout (5 min)

Open https://cloud.nself.org/pricing in a private window and start a checkout. Confirm the checkout page is hosted at checkout.lemonsqueezy.com rather than checkout.stripe.com.

If Stripe Elements appear instead of the LS checkout page, the cloud frontend is caching old provider state. Add a query param to bust the cache: ?_bust=$(date +%s).

6. Update status page (1 min)

Post to status.nself.org (or nSentry status page if live):

Billing: switched to backup payment provider. Existing subscriptions unaffected.

Coexistence during switchover

While PAYMENT_PROVIDER=lemonsqueezy:

  • New checkouts go through Lemon Squeezy
  • Existing Stripe subscriptions keep renewing — Stripe webhooks (/webhooks/stripe) continue processing renewals, cancellations, and payment failures
  • The /webhooks/lemonsqueezy endpoint handles new LS events in parallel
  • License activation works identically for both providers

There is no need to migrate existing Stripe subscribers to Lemon Squeezy. They stay on Stripe until they cancel; new sign-ups go through LS.

Reverting to Stripe

curl -s -X POST \
  -H "X-Admin-Key: $PING_API_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"provider":"stripe"}' \
  https://ping.nself.org/admin/payment-provider | jq .

Environment variable alias

The internal switch uses PAYMENT_PROVIDER. The admin panel and cloud frontend also read BILLING_PROVIDER as an alias for display purposes — set either one. The factory in payments/factory.ts reads PAYMENT_PROVIDER as the authoritative source; BILLING_PROVIDER is surfaced in health checks only.

Troubleshooting

SymptomCheck
lemonsqueezy_configured: falseMissing one of the three required LS env vars
Checkout still goes to StripeVercel may be caching a CDN response — check PAYMENT_PROVIDER in Vercel env
Webhook signature failuresLEMONSQUEEZY_WEBHOOK_SECRET mismatch — regenerate in LS dashboard and redeploy
License not activated after purchaseCheck np_payment_events for the LS event ID; check ping_api logs
503 Lemon Squeezy not fully configuredOne of the three required vars is missing from the running ping_api process