How do I handle Stripe and Lemon Squeezy webhooks idempotently in Next.js?
It depends on your billing providers, but the core architecture is identical: verify the raw payload signature before parsing JSON, normalize provider events into a shared internal contract, and enforce idempotency at the database boundary using the provider's event or order ID. Never grant product entitlements from a client-side checkout redirect; let the verified webhook transactionally update subscription state and entitlements once. supastack prewires this pipeline for Stripe, Lemon Squeezy, and mobile RevenueCat, though single-plan apps can start with a simpler handler.
JSON'a dokunmadan önce ham yükü doğrulayın
Her ödeme sağlayıcısı kablo üzerinden gönderdiği bayt dizisinin tamamını imzalar. Bir rota işleyicisi imzayı kontrol etmeden önce await request.json() ile veriyi ayrıştırıp nesneyi yeniden serileştirirse, anahtar sırası veya boşluk farkları geçerli webhook'ları geçersiz kılar ya da geliştiricileri hazırlık ortamında doğrulamayı atlamaya iter.
Next.js App Router POST işleyicinizde ham gövdeyi await request.text() ile bir kez okuyun, kriptografik imzayı yalnızca sunucuda bulunan webhook gizli anahtarıyla doğrulayın ve tek bir veritabanı sorgusu bile çalıştırmadan önce başarısız olan her isteği HTTP 400 ile reddedin. Faturalandırma modülü rehberi, supastack'in imza kontrollerini tek bir BFF giriş noktası arkasında nasıl izole ettiğini gösterir.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) return new Response("Missing signature", { status: 400 });
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
env.STRIPE_WEBHOOK_SECRET,
);
await handleNormalizedBillingEvent(event);
return Response.json({ received: true });
}- Ham istek gövdesini
request.text()ile okuyun — ayrıştırılmış JSON'u asla yeniden dizge haline getirmeyin. STRIPE_WEBHOOK_SECRETve Lemon Squeezy imzalama sırlarını yalnızca sunucu yapılandırmasında tutun.- HMAC imzalarını manuel olarak doğrularken sabit zamanlı karşılaştırma (
crypto.timingSafeEqual) kullanın.
Tekilleştirmeyi bellekte değil, veritabanı sınırında yapın
Stripe ve Lemon Squeezy; bir dağıtım yeniden başladığında, sunucusuz bir fonksiyon zaman aşımına uğradığında veya bir ağ sıçraması yanıtı düşürdüğünde webhook'ları yeniden dener. Süreç içi kümeler veya kısa ömürlü önbellek anahtarları örnekler arasında kaybolur ve iki eşzamanlı yeniden denemenin çift lisans vermesini veya iki makbuz e-postası göndermesini engellemez.
Sağlayıcının olay kimliğini veya sipariş kapatma durumunu, abonelik erişimini değiştiren aynı Postgres işlemi içinde kaydedin. Atomik bir INSERT ... ON CONFLICT DO NOTHING veya status != 'paid' koşullu güncellemesi, eşzamanlı bir tekrarı güvenli bir etkisiz işleme dönüştürür. Bu webhook'ların yerine getirdiği planları tasarlarken, SaaS fiyatlandırma sayfası oluşturucu kademeleri ve koltuk kurallarını önceden modellemenize yardımcı olur.
const settled = await db.transaction(async (tx) => {
const [inserted] = await tx
.insert(billingWebhookEvents)
.values({ providerEventId: event.id, provider: "stripe" })
.onConflictDoNothing({ target: billingWebhookEvents.providerEventId })
.returning({ id: billingWebhookEvents.id });
if (!inserted) return false; // Already processed
await applyWorkspaceEntitlement(tx, event);
return true;
});Sırası karışmış olayları ve çakışan abonelikleri yönetin
Webhook'ların kullanıcının tıkladığı sırayla ulaşacağı garanti edilmez. customer.subscription.updated olayı checkout.session.completed olayından önce gelebilir ve yanlışlıkla iki ödeme sekmesi açan bir müşteri aynı çalışma alanında kısa süreliğine iki aboneliğe sahip olabilir.
İptalleri ve ödeme hatalarını olayı tetikleyen belirli abonelik kimliğiyle sınırlandırın. Bir çalışma alanının ücretli erişim hakkını iptal etmeden önce, başka bir etkin aboneliğin o çalışma alanını hâlâ kapsayıp kapsamadığını kontrol edin; aksi takdirde yinelenen bir aboneliği iptal etmek, müşterinin ödemeye devam ettiği erişimi sessizce ortadan kaldırır.
- Abonelik durumunu yalnızca
workspaceIdile değil,providerSubscriptionIdile anahtarlayın. - En az bir abonelik
activeveyatrialingolduğu sürece çalışma alanının ücretli erişim hakkını etkin tutun. - Tek seferlik siparişleri karşılamadan önce ödenen tutarın ve para biriminin beklenen SKU ile eşleştiğini doğrulayın.
SaaS faturalandırma webhook'ları hakkında sık sorulan sorular
Bu sorular, bir ekip faturalandırmayı sandbox ödeme testlerinden gerçek üretim trafiğine taşıdığında ortaya çıkar.
- Ödeme başarı yönlendirmesinde ücretli özellikleri açabilir miyim? — Hayır. Yönlendirme URL'leri yer imlerine eklenebilir, paylaşılabilir veya eşzamansız ödeme yöntemleri tamamlanmadan önce ziyaret edilebilir. Doğrulanmış webhook erişimi verirken başarı sayfasını sunucu durumunu yoklamak için kullanın.
- Yinelenen bir webhook hangi durum kodunu döndürmelidir? — Veritabanınız olayın zaten işlendiğini doğruladığı anda
200 OKdöndürün.4xxveya5xxdöndürmek sağlayıcıya yeniden denemeye devam etmesini söyler. - Webhook yeniden denemelerini yerel olarak nasıl test ederim? — Birebir aynı imzalı yükü iki kez gönderen deterministik birim testleri yazın ve yalnızca tek bir lisans, erişim hakkı veya işlemsel e-posta üretildiğini doğrulayın.
- Mobil uygulama içi satın alımlar aynı modele nasıl uyar? — Web ve mobil satın alımların birbirinin üzerine yazmaması için RevenueCat sunucu bildirimlerini aynı idempotent erişim hakkı tablosu üzerinden yönlendirin; yayın öncesi kontroller için lansman kontrol listesine bakın.