NextJS 14: Stripe Webhook.

Integrating and Handling Stripe Webhooks in Next.js

by Tego

Links

Twitter/X@tegodotdev

Portfoliotego.dev

Tweet: https://twitter.com/tegocodes/status/1747511299657916604

#nextjs, #stripe, #stripe-checkout, #subscription, #api, #template, #webhook

stripe_logo

Integrating Stripe webhooks into a Next.js application can be a game-changer, especially for e-commerce platforms. At the same time it can be a nightmare. In this blog post, I'll share a comprehensive guide on setting up a route to handle Stripe Webhooks in Next.js.

The Setup

Let's start by cloning the webhook file to your project. Using curl and a few other commands we can quickly get the file cloned over into your project.

MacOS/Unix:

Terminal:

mkdir -p src/app/api/webhooks/billing/stripe
chmod -R 755 src/app/api/webhooks/billing/stripe
curl -o src/app/api/webhooks/billing/stripe/route.ts https://raw.githubusercontent.com/tego101/nextjs-14-stripe-webhooks/main/stripe-webhooks.ts

Windows PowerShell 😬:

New-Item -ItemType Directory -Force -Path .\src\app\api\webhooks\billing\stripe
curl -o .\src\app\api\webhooks\billing\stripe\route.ts https://raw.githubusercontent.com/tego101/nextjs-14-stripe-webhooks/main/stripe-webhooks.ts

Defining Event Types

Here we will look over the event types in the file and apply our logic.

Here are the event types in our webhook file:

Stripe uses webhooks to notify your server about events that happen in your account, such as successful payments, customer updates, and more. These webhooks payloads have event identifiers, take a look below.

You can click on them to find out more about each event type.

Handling Events.

For this I highly recommend using NextJS Server Actions. These serverside functions are game changing and allow you to securely do things like handle billing without exposing routes or info.

In my case this is how I handled the "checkout.session.completed" event. I found the event case in the webhook file and I added my logic to it using Server Actions.

import {
    getInvoice,
    addInvoicePayment,
    markInvoicePaid,
    getInvoicePayment,
    getInvoicePaymentByStripeID,
    updateInvoicePayment,
    
} from '@/actions/invoice-actions.ts';

import {
    renewOrCreateService,
} from '@/actions/subscriber-service-actions.ts';

case "checkout.session.completed":
    if (status !== "paid") {
        // TODO: Failed Payment.
        
        return new Response(
            JSON.stringify({ error: "Payment not completed!" }),
            {
                status: 500,
            }
        );
    }

    if (meta?.is_service_renewal) {
        // TODO: Handle service renewal.
    }

    // Find invoicePayment by Stripe Checkout Session ID.
    const invoicePayment = await getInvoicePaymentByStripeID(body.data?.object?.client_reference_id.toString())

    if (!invoicePayment) {
        return new Response(JSON.stringify({ error: "Payment not found!" }), {
            status: 500,
        });
    }

    // Add payment_intent to invoicePayment.meta
    const updatedInvoicePayment = await addInvoicePayment({
            status: "PAID",
            stripeCheckoutSessionId: id,
            stripeCheckoutPaymentIntentId: body.data?.object?.payment_intent ?? null,
            stripeCheckoutInvoiceId: stripe_invoice ?? null,
    });
    
    // Update invoice status to paid
    const updateInvoice = await markInvoicePaid(
        invoicePayment.invoiceId,
        {
            stripeCheckoutSessionId: id,
            stripeCheckoutInvoiceId: stripe_invoice ?? payment_intent,
        },
    });

    // Activate service and set expiresAt to the plan.duration & plan.durationUnit + now
    // If mode is set to subscription, set the stripeSubscriptionId to the subId
    // Finally, set the status to ACTIVE
    const service = await renewOrCreateService(meta?.serviceId,
        {
            expiresAt: calculateExpirationDate(invoice?.plan as Plan),
            status: "ACTIVE",
            stripeSubscriptionId: mode !== "payment" ? subId : null,
            meta: {
                stripe_invoice: stripe_invoice,
                stripe_checkout_session_id: id,
                stripe_checkout_invoice_id: stripe_invoice,
                stripe_payment_intent: payment_intent,
            },
        },
    });

    // Email the user to let them know their subscription is active.
    // Resend sendSubWelcomeEmail() server action here.
    return new Response(JSON.stringify({ message: "Payment completed!" }), {
        status: 200,
    });

In this snippet I update the invoice with the new stripe payment id. I also create a new Invoice Payment and store that aswell, right before I renew or create the subscription for the subscriber.

Go ahead and Explore!

Inspect the webhook file and add in your server actions or logic you have for your app. I have included a few console.log events in the file so you may get a visual of what data is coming in when the webhook is triggered.

Also, Don't forget to set your environment values:

STRIPE_WEBHOOK_SECRET : The secret key ensures the security and integrity of the webhook payloads.

Hope this helps someone, -- Tego