API & Backend

Handling Asynchronous Payment Webhooks Safely: Idempotency, Signature Verification, and Retry Queues in Next.js

Laurince Quijano
Laurince Quijano
August 12, 20267 min read
Handling Asynchronous Payment Webhooks Safely: Idempotency, Signature Verification, and Retry Queues in Next.js

In modern e-commerce and SaaS architectures, checkout flows rely heavily on asynchronous event notifications (Webhooks). When a customer completes a payment via Stripe, PayPal, or LemonSqueezy, the gateway asynchronously notifies your backend server via an HTTP POST payload.

However, webhooks operate over unreliable public networks. Network spikes, server timeouts, or accidental duplicate dispatches by payment gateways can cause your webhook endpoint to receive the exact same checkout.session.completed event multiple times.

Without defensive webhook engineering, unhandled webhook retries can lead to duplicate order fulfillment, corrupted user subscriptions, or unauthorized privilege escalations.

Here is a technical blueprint for building resilient, idempotent, and secure payment webhook handlers in Next.js.


The Three Pillars of Webhook Defense

1. Cryptographic Signature Verification Never trust unauthenticated HTTP payloads. Payment gateways sign every webhook payload using a shared secret key and transmit the signature in the request headers (e.g. Stripe-Signature). Your server must re-compute the HMAC hash using the raw request body buffer before parsing JSON.

2. Idempotency Key Tracking Payment gateways guarantee at-least-once delivery, meaning you WILL receive duplicate payloads. An Idempotent Handler ensures that processing an event 10 times yields the exact same state result as processing it once. Store processed event IDs (evt_1N...) in a fast Redis cache or SQL database table.

3. Fast Response ACK (200 OK) vs. Asynchronous Task Processing Gateways expect your webhook route to return an HTTP 200 OK within 3 to 5 seconds. If your handler performs slow external API calls, PDF generation, or email dispatches, the gateway will time out and trigger an automatic retry storm. Separate event ingestion from background execution using task queues.


Step-by-Step Implementation in Next.js Route Handlers

1. Verifying HMAC Signatures on Raw Payloads In Next.js App Router, you must read the raw binary body via request.text() before any JSON parsing occurs:

typescript
// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(request: NextRequest) {
  const body = await request.text(); // Retrieve raw request payload
  const signature = request.headers.get("stripe-signature");

  if (!signature) {
    return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 });
  }

  let event: Stripe.Event;

  try {
    // Cryptographically verify payload signature
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err: any) {
    console.error(`Webhook signature verification failed: ${err.message}`);
    return NextResponse.json({ error: `Webhook Error: ${err.message}` }, { status: 400 });
  }

  // Pass verified event to processing handler
  return handleStripeEvent(event);
}

2. Enforcing Idempotency via Redis or SQL Locks Prevent duplicate processing by caching event IDs before triggering database mutations:

typescript
import { redis } from "@/lib/redis"; // Redis client connection

async function handleStripeEvent(event: Stripe.Event) {
  const eventId = event.id;
  const lockKey = `webhook:processed:${eventId}`;

  // 1. Check if event ID has already been processed (EXISTS in Redis)
  const isProcessed = await redis.get(lockKey);
  if (isProcessed) {
    console.log(`Duplicate webhook event detected: ${eventId}. Skipping execution.`);
    return NextResponse.json({ received: true, status: "duplicate_ignored" });
  }

  // 2. Process specific event types
  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      await fulfillOrder(session);
      break;
    }
    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await revokeSubscriptionAccess(subscription.id);
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // 3. Mark event as processed with a 72-hour TTL expiration
  await redis.set(lockKey, "completed", "EX", 259200);

  return NextResponse.json({ received: true });
}

Webhook Resilience & Verification Matrix

Vulnerability / RiskWithout SafeguardsWith Defensive Webhook Pipeline
Spoofed PayloadsAttacker posts fake payment_success payloads.Rejected by constructEvent() HMAC verification.
Duplicate DeliveryCustomer receives duplicate licenses/credits.Bypassed silently via Redis idempotency lock.
Gateway TimeoutServer takes 6s to process; Gateway retries 5x.Returns instant 200 OK & defers processing.
Payload Replay AttackStolen old payload re-sent by attacker.Rejected due to timestamp tolerances in signature.

Best Practices for Production Billing Handlers

  1. Configure Replay Attack Time Windows: Cryptographic signature checkers automatically reject webhooks older than 5 minutes (tolerance: 300) to defend against replay attacks.
  2. Separate Test and Live Webhook Secrets: Store distinct secrets for development (whsec_test_...) and production (whsec_live_...) to prevent staging events from affecting live user accounts.
  3. Log Unhandled Events: Always return a 200 OK for unhandled event types (e.g. charge.succeeded) so payment gateways do not interpret them as failed server errors.

By enforcing HMAC signature verification, atomic idempotency checks, and fast asynchronous execution, your Next.js payment infrastructure remains 100% resilient against network fluctuations and duplicate event storms!

Laurince Quijano
Written By

Laurince Quijano

Full-Stack Architect

Award-winning Web Developer with 10+ years of experience. Specializing in enterprise Next.js performance, custom Magento plugins, API integrations, and Technical SEO infrastructure.

Need premium developer consulting?

Let's discuss how we can build API split checkouts, dynamic Next.js interfaces, or speed audits for your business.

Get in Touch