RecipesStripe webhook

Setting up a Stripe webhook with Kaito

An advantage of Kaito moving to support Request/Response APIs in v3 is that it made it super easy to setup a Stripe webhook. You can read more at the bottom of this page about why that was the case.

Example

context.ts
import {create} from '@kaito-http/core';
 
export const router = create({
	getContext: async (req, head) => {
		return {
			bodyAsText: () => req.text(),
		};
	},
});
stripe.ts
import {router} from './context.ts';
import {KaitoError} from '@kaito-http/core';
import stripe from '@stripe/stripe-js';
 
// Create a crypto provider for stripe to use, required in some runtimes that don't define `crypto.subtle` globally.
// If you're unsure, try without, and then bring it back if you get an error.
const webCrypto = stripe.createSubtleCryptoProvider();
 
// Notice how we don't define a body schema, we're using stripe's webhook logic to parse the body for us
// which requires the raw body as a string.
export const stripe = router.post('/webhook', async ({ctx}) => {
	const sig = ctx.req.headers.get('stripe-signature');
 
	if (!sig) {
		throw new KaitoError(400, 'No signature provided');
	}
 
	const body = await ctx.bodyAsText();
 
	const event = await stripe.webhooks.constructEventAsync(
		body,
		sig,
		process.env.STRIPE_ENDPOINT_SECRET!, // You should validate this exists, and not use the `!` operator
		undefined,
		webCrypto,
	);
 
	// Handle different event types
	switch (event.type) {
		case 'payment_intent.succeeded':
			// Handle successful payment
			break;
		// Add other event types as needed
	}
});
💡

For production webhooks, always implement proper error handling and logging. Stripe will retry failed webhook deliveries, so make sure your endpoint is idempotent and can handle duplicate events.

What did v3 change?

Prior to Kaito v3, handling raw request bodies (like those required by Stripe webhooks) was challenging. The request object was a Node.js IncomingMessage, and body parsing happened automatically before your route code executed. This created problems because:

  1. The request body was already consumed by the time your route code ran
  2. There was no obvious way to access the raw, unparsed body that Stripe’s webhook verification requires

Kaito v3 solved this with the Fetch API

  • Access the raw request body on demand using req.text()
  • Control when and how body parsing happens
  • Work with the raw request data directly when needed

This makes implementing Stripe webhooks (and other services requiring raw body access) much more straightforward and less error-prone.