Alt description missing in image
Beta: Plugins coming soon!
Alt description missing in image

stripeWebhook()

Validates incoming Stripe webhook events and triggers syncStripeData() for relevant payment events to keep your DB entitlements in sync.

A webhook endpoint is automatically set up for you at /api/webhooks/stripe

Usage - Webhook

This route is already set up for you:

apps/next/app/api/webhooks/stripe/route.ts
import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
import { stripeWebhook } from '@payments/stripe/resolvers/stripeWebhooks.resolver'
 
/* --- Routes ---------------------------------------------------------------------------------- */
 
export const POST = createNextRouteHandler(stripeWebhook, false)

Implementation details

  • Validates the Stripe webhook signature using STRIPE_WEBHOOK_SECRET.
  • Extracts the event from the request body.
  • Processes relevant events asynchronously via waitUntil().
  • Calls syncStripeData() for each tracked event with the customer’s customerId.

Tracked events — The webhook listens for the following Stripe events:

stripeWebhooks.resolver.ts
export const TRACKED_EVENTS: Stripe.Event.Type[] = [
    'checkout.session.completed',
    'customer.subscription.created',
    'customer.subscription.updated',
    'customer.subscription.deleted',
    'customer.subscription.paused',
    'customer.subscription.resumed',
    'customer.subscription.pending_update_applied',
    'customer.subscription.pending_update_expired',
    'customer.subscription.trial_will_end',
    'invoice.paid',
    'invoice.payment_failed',
    'invoice.payment_action_required',
    'invoice.upcoming',
    'invoice.marked_uncollectible',
    'invoice.payment_succeeded',
    'payment_intent.succeeded',
    'payment_intent.payment_failed',
    'payment_intent.canceled',
]
stripeWebhooks.resolver.ts
export const stripeWebhook = createResolver(async ({ req, tryCatch, withDefaults }) => {
 
    const body = await req.text()
    const sig = (await headers()).get('Stripe-Signature')
 
    // Validate webhook secret exists
    if (!process.env.STRIPE_WEBHOOK_SECRET) {
        console.error('[STRIPE] -!- No webhook secret in env vars')
        return NextResponse.json({}, { status: 500 })
    }
 
    if (!sig) return NextResponse.json({}, { status: 400 })
 
    // Process event asynchronously
    const startEventProcessing = async () => {
        const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
        waitUntil(processEvent(event))
    }
 
    const { error } = await tryCatch(startEventProcessing())
    if (error) console.error('[STRIPE] -!- Webhook error:', error)
 
    return withDefaults({ received: true })
})

Input & Output

The webhook input schema is empty, as data must come in from the raw request body.

For the output though, we send a simple recieved flag:

import { StripeWebhookBridge } from '@payments/stripe/resolvers/stripeWebhooks.resolver'
export const StripeWebhookBridge = createDataBridge({
    resolverName: 'stripeWebhook',
    inputSchema: schema('StripeWebhooksInput', {}),
    outputSchema: schema('StripeWebhooksOutput', { received: z.boolean() }),
    apiPath: '/api/stripe/webhooks',
    allowedMethods: ['POST'],
    isMutation: true,
})

Testing webhooks locally

Install the stripe cli first, then run this:

stripe listen --forward-to http://localhost:3000/api/webhooks/stripe

This will forward Stripe CLI webhook events to your local development server.

Recommendations

💡

Make sure you’ve added your webhook endpoint URL (https://{your-domain}/api/webhooks/stripe) in the Stripe Dashboard settings for both test and live environments, and added the webhook signing secret to your env vars as STRIPE_WEBHOOK_SECRET.

The webhook handler processes events asynchronously, so it responds immediately to Stripe while data sync happens in the background. This prevents webhook timeouts while keeping your entitlements up to date.