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:
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’scustomerId.
Tracked events — The webhook listens for the following Stripe events:
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',
]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/stripeThis 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.
