Skip to main content
The Stripe webhook endpoint receives and processes events from Stripe to keep your billing state synchronized.

Endpoint

POST /api/webhook/stripe This endpoint verifies the Stripe signature and handles various billing lifecycle events.

Authentication

This endpoint uses Stripe webhook signature verification instead of user authentication. You must configure the STRIPE_WEBHOOK_SECRET environment variable with your webhook signing secret from the Stripe dashboard.

Headers

stripe-signature
string
required
The Stripe webhook signature header for event verification

Request body

The request body is the raw Stripe event payload. Stripe sends different event types with varying data structures.

Supported events

The webhook currently handles these Stripe event types:

checkout.session.completed

Triggered when a customer completes a checkout session.
packages/landing/app/api/webhook/stripe/route.ts
case "checkout.session.completed": {
  const stripeObject: Stripe.Checkout.Session = event.data
    .object as Stripe.Checkout.Session;

  const session = await findCheckoutSession(stripeObject.id);
  const priceId = session?.line_items?.data[0]?.price?.id;
  const plan = configFile.stripe.plans.find(
    (p) => p.priceId === priceId
  );

  if (!plan) break;

  // TODO: Grant access to the product via Supabase
  // const userId = stripeObject.client_reference_id;
  // Update user in Supabase with hasAccess = true, customerId, priceId, etc.

  break;
}
The TODO comments indicate where you should integrate with your Supabase database to grant product access to the user.

checkout.session.expired

Triggered when a checkout session expires without completion.
case "checkout.session.expired":
  // Handle expired checkout sessions
  break;

customer.subscription.updated

Triggered when a subscription is updated (plan change, renewal, etc.).
case "customer.subscription.updated":
  // TODO: Update subscription status in database
  break;

customer.subscription.deleted

Triggered when a subscription is canceled or expires.
case "customer.subscription.deleted": {
  // TODO: Revoke access via Supabase
  break;
}

invoice.paid

Triggered when an invoice payment succeeds (subscription renewal, etc.).
case "invoice.paid": {
  // TODO: Grant access via Supabase
  break;
}

invoice.payment_failed

Triggered when an invoice payment fails.
case "invoice.payment_failed":
  // Handle payment failures (email customer, retry logic, etc.)
  break;

Response

Success (200)

{}
Returns an empty JSON object to acknowledge receipt of the webhook event.

Error (400)

{
  "error": "Webhook signature verification failed. Invalid signature."
}
Returned when the Stripe signature verification fails, indicating the request may not be from Stripe.

Implementation notes

The webhook handler verifies the Stripe signature before processing any events:
packages/landing/app/api/webhook/stripe/route.ts
const body = await req.text();
const signature = (await headers()).get("stripe-signature");

try {
  event = stripe.webhooks.constructEvent(body, signature!, webhookSecret);
} catch (err: any) {
  console.error(`Webhook signature verification failed. ${err.message}`);
  return NextResponse.json({ error: err.message }, { status: 400 });
}
Always verify webhook signatures in production. This prevents unauthorized parties from triggering billing actions in your system.

Setup

1. Configure webhook in Stripe

  1. Go to the Stripe Dashboard → DevelopersWebhooks
  2. Click Add endpoint
  3. Enter your webhook URL: https://yourdomain.com/api/webhook/stripe
  4. Select the events you want to receive (or select “all events” for development)
  5. Copy the Signing secret

2. Add environment variable

Add the webhook signing secret to your environment variables:
STRIPE_WEBHOOK_SECRET=whsec_...

3. Test locally with Stripe CLI

Use the Stripe CLI to forward webhook events to your local development server:
stripe listen --forward-to localhost:3000/api/webhook/stripe
The CLI will output a webhook signing secret for local testing. Use this in your .env.local file.

4. Trigger test events

stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.paid

Integration checklist

The current implementation includes TODO comments where you need to integrate with your database:
  • checkout.session.completed: Grant product access in Supabase
  • customer.subscription.updated: Update subscription status
  • customer.subscription.deleted: Revoke access
  • invoice.paid: Confirm or extend access
  • invoice.payment_failed: Notify customer and handle grace period
Store the Stripe customer_id, subscription_id, and price_id in your user profiles table to track billing status.

Security considerations

  • Signature verification: Always verify the stripe-signature header
  • Idempotency: Handle duplicate webhook deliveries gracefully
  • Logging: Log webhook events for debugging and audit trails
  • Error handling: Return 200 even if your internal processing fails (Stripe will retry)

Billing configuration

Configure Stripe API keys and webhook secrets

Checkout API

Create checkout sessions and customer portals

Build docs developers (and LLMs) love