Lemon Squeezy webhooks in Next.js: idempotent billing without double charges
A first-timer guide to HMAC verification, Postgres idempotency, and handling refunds or subscription cancellations in a SaaS boilerplate.

Selling a digital product through your own Next.js app means one scary weekend: webhooks.
Lemon Squeezy retries events. Without idempotency you double-grant access. Without refund handling you keep charging for revoked products.
Here is the production pattern we ship in the Vibe PWA Starter-kit.
First-time setup (15 minutes)
- Create products + variants in Lemon Squeezy
- Set env vars: API key, store ID, webhook secret, variant IDs
- Register
POST /api/webhooks/lemon-squeezyin the Lemon dashboard (or use the kit provision script)
Events wired by default: order_created, order_refunded, subscription_created, subscription_updated, subscription_cancelled, subscription_expired.
What each request does
- Verify
x-signatureHMAC against your webhook secret - Claim the event ID in Postgres (
claim_lemon_webhook_event) — duplicates return200safely - Upsert
billing_subscriptionswith the mapped plan code - Revoke access on
order_refunded,subscription_cancelled,subscription_expired
deriveAccessGrant() is the single gate for protected routes and download links.
Gotchas for newcomers
| Gotcha | Fix |
| ----------------------------- | -------------------------------------------------------------- |
| Variant ID ≠ plan code | Map variant IDs to catalog plan_code in env + catalog |
| Testing in live mode | Use Lemon test mode until checkout works end-to-end |
| Webhook URL on preview deploy | Point webhooks to production URL only |
| Refund without webhook | Lemon retries — idempotent claim prevents double revoke errors |
Ship faster
The starter-kit includes migrations, route handler, admin client, and START-HERE checklist — so you configure keys instead of researching edge cases.
Passer du papier au système
Ce guide pose le diagnostic ; le pack correspondant livre les fichiers, prompts et checklists pour exécuter sans friction.