fastify-xstripe
Webhook Route
The POST webhook endpoint registered by fastify-xstripe — signature verification, event dispatch, and custom handler authoring.
Webhook Route
The plugin registers a single POST route that receives all Stripe webhook events, verifies the signature, and dispatches each event to the appropriate handler.
Route
POST /stripe/webhook (default, configurable via webhookPath option)
Handler Signature
Custom handlers passed via the handlers option receive three arguments:
async function handler(
event: Stripe.Event, // the verified Stripe event object
fastify: FastifyInstance, // Fastify instance (access logger, other decorators)
stripe: Stripe // Stripe SDK client (make additional API calls)
): Promise<void>
How Dispatch Works
- Reads the raw request body and
stripe-signatureheader. - Calls
stripe.webhooks.constructEvent(rawBody, sig, webhookSecret)— returns HTTP 400 on failure. - Looks up
handlers[event.type]. User handlers override defaults via object spread. - Calls the matched handler with
(event, fastify, stripe). - Always returns HTTP 200, even if the handler throws — Stripe interprets non-200 as a retry signal.
Request / Response
Request headers:
| Header | Required | Description |
|---|---|---|
stripe-signature | Yes | HMAC signature sent by Stripe. |
Successful response:
{ "received": true, "processed": true }
Handler threw an error:
{ "received": true, "processed": false, "error": "handler error message" }
Signature missing (HTTP 400):
{ "error": "[xStripe] Missing stripe-signature header" }
Signature invalid (HTTP 400):
{ "error": "[xStripe] Webhook signature verification failed: ..." }
Custom Handlers
Override any default handler or add handlers for events not in the default set:
await fastify.register(xStripe, {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
handlers: {
// Override a default handler
"customer.subscription.created": async (event, fastify, stripe) => {
const sub = event.data.object;
// Provision the user's account
await db.subscriptions.upsert({
where: { stripeCustomerId: sub.customer },
create: {
stripeSubscriptionId: sub.id,
status: sub.status,
planId: sub.items.data[0]?.price.id,
},
update: { status: sub.status },
});
},
// Add a handler for an event not in the defaults
"customer.discount.created": async (event, fastify, stripe) => {
const discount = event.data.object;
fastify.log.info({ discountId: discount.id }, "Discount applied");
},
},
});
Handling Errors in Handlers
If a handler throws, the plugin logs the error and returns { received: true, processed: false } with HTTP 200. This prevents Stripe from retrying the delivery. Implement your own retry logic inside the handler if needed:
"invoice.payment_failed": async (event, fastify, stripe) => {
const invoice = event.data.object;
try {
const customer = await stripe.customers.retrieve(invoice.customer);
await sendEmail(customer.email, "Payment failed — please update your payment method.");
} catch (err) {
// Log but do NOT re-throw — let the route return 200
fastify.log.error({ err, invoiceId: invoice.id }, "Failed to send payment-failure email");
}
}
Testing Locally
# Install the Stripe CLI, then forward events to your local server:
stripe listen --forward-to localhost:3000/stripe/webhook
# Trigger specific test events:
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
stripe trigger checkout.session.completed
See Also
- Helpers — Utility functions for parsing Stripe event data.
- Default event handlers — The 23 built-in handlers in
index.md.
AI Context
package: "@xenterprises/fastify-xstripe"
route: POST /stripe/webhook (configurable via webhookPath)
use-when: Stripe webhook handler with automatic signature verification — dispatches to built-in or custom event handlers
signature-header: stripe-signature (verified with stripe.webhooks.constructEvent)
handler-signature: async (event, fastify, stripe) => void
returns: HTTP 200 always (errors are logged, not re-thrown, to prevent Stripe retries)
