fastify-xstripe
fastify-xstripe
Stripe webhook handling for Fastify v5. Registers a POST webhook route with automatic signature verification, dispatches to 23 built-in subscription/invoice/payment/checkout/charge event handlers (all overridable), and decorates fastify.stripe with the full Stripe SDK client.
Installation
npm install @xenterprises/fastify-xstripe stripe
Quick Start
import Fastify from "fastify";
import xStripe from "@xenterprises/fastify-xstripe";
const fastify = Fastify({ logger: true });
await fastify.register(xStripe, {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
});
// Full Stripe SDK available anywhere
const customer = await fastify.stripe.customers.create({ email: "user@example.com" });
await fastify.listen({ port: 3000 });
Options
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
apiKey | string | — | Yes | Stripe secret API key (sk_test_... or sk_live_...). |
webhookSecret | string | — | Yes | Stripe webhook signing secret (whsec_...). |
webhookPath | string | "/stripe/webhook" | No | Path where the webhook POST route is registered. |
handlers | object | {} | No | Custom event handlers that override the defaults. |
apiVersion | string | "2024-11-20.acacia" | No | Stripe API version string. |
Decorated Properties
| Property | Type | Description |
|---|---|---|
fastify.stripe | Stripe | The initialized Stripe SDK client — use it to call any Stripe API. |
Pages
- Webhook Route — The
POST /stripe/webhookhandler: signature verification, dispatch, custom handler authoring, and testing. - Helpers — Utility functions for formatting amounts, resolving subscription state, extracting invoice data, and more.
Custom Handlers
Override any default handler with your business logic. Each handler receives (event, fastify, stripe):
await fastify.register(xStripe, {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
handlers: {
"customer.subscription.created": async (event, fastify, stripe) => {
const sub = event.data.object;
await db.users.update({
where: { stripeCustomerId: sub.customer },
data: { subscriptionId: sub.id, status: sub.status },
});
},
"invoice.payment_failed": async (event, fastify, stripe) => {
const invoice = event.data.object;
const customer = await stripe.customers.retrieve(invoice.customer);
await sendEmail(customer.email, "Payment Failed", "Please update your card.");
},
},
});
Default Event Handlers
All 23 built-in handlers log structured data via fastify.log. Override any via the handlers option.
Subscription Events
| Event | Logged Fields |
|---|---|
customer.subscription.created | subscriptionId, customerId, status, planId |
customer.subscription.updated | subscriptionId, customerId, status, previous changes |
customer.subscription.deleted | subscriptionId, customerId, canceledAt |
customer.subscription.trial_will_end | subscriptionId, customerId, trialEnd |
customer.subscription.paused | subscriptionId, customerId |
customer.subscription.resumed | subscriptionId, customerId |
Invoice Events
| Event | Logged Fields |
|---|---|
invoice.created | invoiceId, customerId, amount, status |
invoice.finalized | invoiceId, customerId, amount |
invoice.paid | invoiceId, customerId, subscriptionId, amount |
invoice.payment_failed | invoiceId, customerId, amount, attemptCount (warn) |
invoice.upcoming | customerId, subscriptionId, amount, periodEnd |
Payment Events
| Event | Logged Fields |
|---|---|
payment_intent.succeeded | paymentIntentId, customerId, amount, currency |
payment_intent.payment_failed | paymentIntentId, customerId, amount, lastPaymentError (warn) |
Customer Events
| Event | Logged Fields |
|---|---|
customer.created | customerId, email |
customer.updated | customerId, previous changes |
customer.deleted | customerId |
Payment Method Events
| Event | Logged Fields |
|---|---|
payment_method.attached | paymentMethodId, customerId, type |
payment_method.detached | paymentMethodId, type |
Checkout Events
| Event | Logged Fields |
|---|---|
checkout.session.completed | sessionId, customerId, subscriptionId, mode, paymentStatus |
checkout.session.expired | sessionId |
Charge Events
| Event | Logged Fields |
|---|---|
charge.succeeded | chargeId, customerId, amount, currency, paymentMethod |
charge.failed | chargeId, customerId, amount, failureCode, failureMessage (error) |
charge.refunded | chargeId, customerId, amountRefunded, refundCount |
Helper Utilities
Import from @xenterprises/fastify-xstripe/helpers or via named export:
import { helpers } from "@xenterprises/fastify-xstripe";
| Helper | Signature | Description |
|---|---|---|
formatAmount(amount, currency) | (number, string) => string | Format Stripe amount to currency string, e.g. 2000, "USD" → "$20.00". |
getPlanName(subscription) | (sub) => string | Get the plan name from a subscription object. |
isActiveSubscription(subscription) | (sub) => boolean | Check if subscription status is "active" or "trialing". |
isInTrial(subscription) | (sub) => boolean | Check if subscription is in trial period. |
getDaysUntilTrialEnd(subscription) | (sub) => number | Days remaining in trial. |
isRenewal(event) | (event) => boolean | Check if an invoice event is a renewal. |
calculateMRR(subscription) | (sub) => number | Calculate MRR in cents. |
getSubscriptionStatusText(status) | (string) => string | Human-readable status, e.g. "active" → "Active". |
getEventDescription(event) | (event) => string | Human-readable event description. |
getCustomerEmail(event, stripe) | (event, stripe) => Promise<string> | Resolve customer email from event. |
isTestEvent(event) | (event) => boolean | Check if event is from test mode. |
getMetadata(event) | (event) => object | Extract metadata from event. |
getPaymentMethodType(paymentMethod) | (pm) => string | Human-readable payment method type, e.g. "Card". |
getInvoiceLineItems(invoice) | (invoice) => array | Get line items from invoice. |
isSubscriptionInvoice(invoice) | (invoice) => boolean | Check if invoice is subscription-related. |
getNextBillingDate(subscription) | (sub) => Date | Get next billing date as a Date object. |
formatDate(timestamp) | (number) => string | Format Unix timestamp to readable date string. |
Error Reference
All errors use the [xStripe] prefix.
Startup Errors
| Error | Cause |
|---|---|
[xStripe] apiKey is required and must be a string | Missing or non-string apiKey. |
[xStripe] webhookSecret is required and must be a string | Missing or non-string webhookSecret. |
[xStripe] webhookPath must be a string | webhookPath is not a string. |
[xStripe] handlers must be a plain object | handlers is not a plain object or is an array. |
[xStripe] apiVersion must be a string | apiVersion is not a string. |
Webhook Runtime Errors (HTTP 400)
| Error | Cause |
|---|---|
[xStripe] Missing stripe-signature header | Webhook request received without a signature header. |
[xStripe] Webhook signature verification failed: ... | Invalid or tampered webhook signature. |
Environment Variables
| Variable | Required | Description |
|---|---|---|
STRIPE_API_KEY | Yes | Stripe secret key (sk_test_... or sk_live_...). Pass as apiKey option. |
STRIPE_WEBHOOK_SECRET | Yes | Webhook signing secret from Stripe Dashboard or CLI (whsec_...). Pass as webhookSecret option. |
How It Works
On registration, the plugin validates all options, initializes the Stripe SDK with the provided apiKey and apiVersion, and decorates fastify.stripe. A POST route is registered at webhookPath that reads the raw request body, verifies the Stripe signature using stripe.webhooks.constructEvent(), then dispatches the event to the resolved handler — user handlers are merged over defaults via object spread ({ ...defaultHandlers, ...handlers }). If a handler throws, the error is logged but the route still returns HTTP 200 to prevent Stripe from retrying the event. The fastify.stripe decorator gives full programmatic access to the Stripe SDK for any API call outside the webhook flow.
Testing Webhooks Locally
# Install Stripe CLI and forward webhooks to your local server
stripe listen --forward-to localhost:3000/stripe/webhook
# Trigger test events
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
stripe trigger checkout.session.completed
AI Context
package: "@xenterprises/fastify-xstripe"
type: fastify-plugin
use-when: Stripe webhook handling with signature verification and full Stripe SDK access
decorator: fastify.stripe (full Stripe SDK client)
webhook-route: POST /stripe/webhook (configurable via webhookPath)
default-handlers: 23 built-in handlers for subscription/invoice/payment/customer/checkout/charge events — all overridable
env: STRIPE_API_KEY, STRIPE_WEBHOOK_SECRET
helper-exports: formatAmount, getPlanName, isActiveSubscription, isInTrial, getDaysUntilTrialEnd, calculateMRR, and more
