Next.js template available

This guide comes with a one-click deployable demo available on GitHub.

When adding payment capabilities to a web application, users expect a fast, reliable, and simple bank confirmation process. However, there is more to payment processing than bank confirmation.

More complex payment processing is normally pushed to the background, such as:

  • Payment failures (No Balance, Card expired, Bank restrictions)
  • Notifications (Upcoming charges, Custom Invoice emails)
  • Customer disputes (Putting holds on charges)

Stripe’s Webhooks

Stripe uses webhooks to provide server-to-server communications between its processes and your back-end systems. This enables you to keep your data up to date asynchronously.

However, its webhooks (subscription.* and other events) come with multiple challenges:

Defer’s implementation of Stripe’s webhooks

Defer offers a solution to the above challenges. With Stripe as its model, Defer provides easier webhook management with the Defer Console and Function Metadata.

The code

First, you need to set up a Stripe client in your API. Then, build a Stripe event from the received webhooks (with stripeClient.webhooks.constructEvent()) and forward it for background processing by calling handleStripeWebhookFn().

app/api/webhooks/route.ts
import type { Stripe } from "stripe";

import { NextResponse } from "next/server";
import { assignOptions } from "@defer/client";

import { stripe } from "@/lib/stripe";
import handleStripeEvent from "defer/handleStripeEvent";

export async function POST(req: Request) {
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      await (await req.blob()).text(),
      req.headers.get("stripe-signature") as string,
      process.env.STRIPE_WEBHOOK_SECRET as string,
    );
  } catch (err) {
    const errorMessage = err instanceof Error ? err.message : "Unknown error";
    // On error, log and return the error message.
    if (err! instanceof Error) console.log(err);
    console.log(`❌ Error message: ${errorMessage}`);
    return NextResponse.json(
      { message: `Webhook Error: ${errorMessage}` },
      { status: 400 },
    );
  }

  // Successfully constructed event.
  console.log("✅ Success:", event.id);

  const permittedEvents: string[] = [
    "checkout.session.completed",
    "payment_intent.succeeded",
    "payment_intent.payment_failed",
  ];

  if (permittedEvents.includes(event.type)) {
    try {
      // attach some metadata to the execution
      //  for better filtering in the Defer Console
      const handleStripeEventWithMetadata = assignOptions(handleStripeEvent, {
        metadata: {
          livemode: event.livemode ? "true" : "false",
          type: event.type,
          apiVersion: event.api_version || "",
        },
      });

      // process the event in the background
      await handleStripeEventWithMetadata(event.id);
    } catch (error) {
      console.error(error);
      return NextResponse.json(
        { message: "Webhook handler failed" },
        { status: 500 },
      );
    }
  }
  // Return a response to acknowledge receipt of the event.
  return NextResponse.json({ message: "Received" }, { status: 200 });
}

The handleStripeEvent() Defer function retrieves the Stripe event using the Stripe client and performs the associated action, for example, updating a customer record.

src/defer/handleStripeEvent.ts
import { defer } from "@defer/client";
import Stripe from "stripe";

// this Background Function processes Stripe events with:
//  - `retries` for resilient processing of events
//  - `concurrency` to avoid external rate limiting during spikes
async function handleStripeEvent(eventID: string) {
  const stripe = new Stripe(process.env.STRIPE_PRIVATE_API_KEY!);
  const event = await stripe.events.retrieve(eventID);
  switch (event.type) {
    case "customer.created":
      break;
    case "customer.deleted":
      break;
    default:
      console.log(`skip ${eventID} event`);
  }
}

export default defer(handleStripeEvent, {
  retry: 7, // enable retries with a exponential back-off strategy
  concurrency: 5,
});

Going further