Webhooks & Integration

Learn how to integrate Stripe’s payment features, webhooks, subscription management, and pricing section in your app.

Stripe’s webhooks notify your app of payment events (e.g., successful payments, subscription updates), and the integration supports checkout sessions, subscription management, and pricing displays, all tied to your Next.js structure and Supabase database via Prisma.

Implementing Payments and Webhooks

Set Up Checkout Sessions

  • Use Stripe’s Checkout in app or components for payments. Example:
@/components/pricing/PricingCard.tsx
import { SignIn } from '@clerk/nextjs';

export default function PricingCard(props) {
  return (
    <form action="/api/stripe/checkout" method="POST" className="w-full">
      <input type="hidden" name="priceId" value={defaultPriceId} />
      {discountedPrice && (
        <input type="hidden" name="couponId" value={couponId} />
      )}
      <Button size={'lg'} className="w-full" type="submit">
        {cta?.text}
      </Button>
    </form>
  );
}
@/app/api/stripe/checkout/route.ts
import { stripe } from '@/lib/stripe/stripe';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(req: NextRequest) {
  const formData = await req.formData();
  const priceId = formData.get('priceId')?.toString();
  const couponId = formData.get('couponId')?.toString();

  try {
    const session = await stripe.checkout.sessions.create({
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      discounts: [
        {
          coupon: couponId,
        },
      ],
      mode: 'payment',
      success_url: `${process.env.SITE_URL}/order?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.SITE_URL}`,
      automatic_tax: {
        enabled: true,
      },
      custom_text: {
        after_submit: {
          message: `By continuing, you agree to Plainform's [Terms of Service](${process.env.SITE_URL}/terms-of-service) and [Privacy Policy](${process.env.SITE_URL}/privacy-policy), and to receive periodic emails with updates.`,
        },
      },
      custom_fields: [
        {
          key: 'github',

          label: {
            type: 'custom',
            custom: 'GitHub Username',
          },
          optional: false,
          type: 'text',
        },
      ],

      payment_intent_data: {
        capture_method: 'manual',
      },
    });

    if (!session.url) {
      return NextResponse.json({
        message: 'Failed to create checkout session.',
        status: 500,
      });
    }

    return NextResponse.redirect(session.url!, { status: 303 });
  } catch (error) {
    return Response.json({ message: 'Server Error', error, status: 500 });
  }
}

Manage Subscriptions

  • Create subscription plans in the Stripe dashboard and integrate them in Checkout. Example:
@/app/api/stripe/checkout/route.ts
const session = await stripe.checkout.sessions.create({
  mode: 'subscription',
  line_items: [{ price: 'price_123', quantity: 1 }],
  success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/success`,
  cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cancel`,
});
  • Allow users to manage subscriptions via Stripe’s Customer Portal. Example:
@/lib/stripe/createPortalSession.ts
import { stripe } from '@/lib/stripe';

export async function createPortalSession(customerId: string) {
  const portalSession = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/account`,
  });
  return portalSession.url;
}

Display Pricing Section

  • Fetch products/prices with getProducts function and render in a pricing component (e.g., components/Pricing.tsx):
@/lib/stripe/getProducts.ts
export async function getProducts() {
  try {
    const res = await fetch(`${process.env.SITE_URL}/api/stripe/products`, {
      cache: 'force-cache',
      next: { tags: ['stripe/products'] },
    });

    if (!res.ok) {
      throw new Error('Failed to fetch products!');
    }

    return res.json();
  } catch (error) {
    return error;
  }
}
@/components/pricing/Pricing.tsx
import locale from '@/locales/en.json';

import { getProducts } from '@/lib/stripe/getProducts';
import { getCoupons } from '@/lib/stripe/getCoupons';

import { PricingCard } from '@/components/pricing/PricingCard';

export async function Pricing() {
  const pricingLocale = locale?.homePage?.pricingSection;

  const data = await getProducts();
  const couponData = await getCoupons();

  if (!data) {
    return null;
  }

  return (
    <div className=" flex items-center justify-center w-full gap-20 ">
      {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
      {data?.products?.map((product: any) => (
        <PricingCard
          key={product?.id}
          name={product?.name}
          description={product?.description}
          imgSrc={product?.images[0]}
          currency={product?.default_price?.currency}
          unitAmount={product?.default_price?.unit_amount}
          defaultPriceId={product?.default_price?.id}
          marketingFeatures={product?.marketing_features}
          priceType={product?.default_price?.type}
          couponId={couponData?.coupon?.id || null}
          amountOff={couponData?.coupon?.amount_off || null}
          percentOff={couponData?.coupon?.percent_off || null}
          isCouponValid={couponData?.coupon?.valid || null}
          perks={pricingLocale?.perks}
          cta={pricingLocale?.cta}
        />
      ))}
    </div>
  );
}

Configure Webhooks

  • In the Stripe dashboard, add a webhook URL (e.g., https://yourapp.com/api/webhooks/stripe) and select events like checkout.session.completed, invoice.paid, and customer.subscription.updated
@/app/api/webhooks/route.ts
  switch (event.type) {
      case 'coupon.created':
        revalidateTag('stripe/coupons');
        break;
      case 'coupon.deleted':
        await recordEvent({
          type: 'coupon',
          clear: true,
        });
        revalidateTag('stripe/coupons');
        revalidateTag('event');
        break;
      case 'product.created':
      case 'product.updated':
      case 'product.deleted':
        revalidateTag('stripe/products');
        break;
      case 'checkout.session.completed':
        revalidateTag('stripe/coupons');
        revalidateTag('stripe/orders');
        await handleCheckoutSessionCompleted(
          event.data.object as Stripe.Checkout.Session
        );
        break;
      default:
        break;

Testing Payments and Webhooks

  • Use Stripe’s test mode (test keys) and run npm run dev to test Checkout and subscriptions.
  • Simulate webhook events in the Stripe dashboard (e.g., checkout.session.completed).
  • Verify the pricing section displays correctly with product data.
  • Test Customer Portal and subscription management flows.

Important Notes

  • Environment Variables: Ensure NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, and STRIPE_WEBHOOK_SECRET are valid, or t3-env will block startup.
  • Customization: Configure Stripe’s Checkout UI or Customer Portal in the dashboard to match your app’s branding.
  • Security: Keep STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET out of client-side code and version control.

For advanced features like multi-currency support, consult the Stripe Documentation.

How is this guide ?

Last updated on