X Enterprises

fastify-xstripe

Fastify plugin for Stripe webhook handling with signature verification, 23 default subscription event handlers, and the Stripe SDK client decorated on the instance as 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.xStripe with the full Stripe SDK client.

Breaking change (pre-1.0 standardization): the Stripe SDK client decorator was renamed from fastify.stripe to fastify.xStripe. Update every fastify.stripe reference in your application when upgrading. Option-validation error messages were also standardized — see the Error Reference.

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,          // consumer owns env access
  webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
});

// Full Stripe SDK available anywhere
const customer = await fastify.xStripe.customers.create({ email: "user@example.com" });

await fastify.listen({ port: 3000 });

The plugin never reads process.env itself — your application reads the environment and passes every value in via options.

Options

NameTypeDefaultRequiredDescription
apiKeystringYesStripe secret API key (sk_test_... or sk_live_...).
webhookSecretstringYesStripe webhook signing secret (whsec_...).
webhookPathstring"/stripe/webhook"NoPath where the webhook POST route is registered. Must start with /.
handlersobject{}NoCustom event handlers keyed by event type. Values must be async functions; they override the default handler for the same event type.
apiVersionstring"2024-11-20.acacia"NoStripe API version passed to the SDK.

All options are validated at registration; invalid or missing options throw before the server starts.

Decorated Properties

PropertyTypeDescription
fastify.xStripeStripeThe initialized Stripe SDK client — use it to call any Stripe API.

Pages

  • Webhook Route — The POST /stripe/webhook handler: 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. Events with no matching handler are acknowledged with processed: false.

Subscription Events

EventLogged Fields
customer.subscription.createdsubscriptionId, customerId, status, planId
customer.subscription.updatedsubscriptionId, customerId, status, previous changes
customer.subscription.deletedsubscriptionId, customerId, canceledAt
customer.subscription.trial_will_endsubscriptionId, customerId, trialEnd
customer.subscription.pausedsubscriptionId, customerId
customer.subscription.resumedsubscriptionId, customerId

Invoice Events

EventLogged Fields
invoice.createdinvoiceId, customerId, amount, status
invoice.finalizedinvoiceId, customerId, amount
invoice.paidinvoiceId, customerId, subscriptionId, amount
invoice.payment_failedinvoiceId, customerId, amount, attemptCount (warn)
invoice.upcomingcustomerId, subscriptionId, amount, periodEnd

Payment Events

EventLogged Fields
payment_intent.succeededpaymentIntentId, customerId, amount, currency
payment_intent.payment_failedpaymentIntentId, customerId, amount, lastPaymentError (warn)

Customer Events

EventLogged Fields
customer.createdcustomerId, email
customer.updatedcustomerId, previous changes
customer.deletedcustomerId

Payment Method Events

EventLogged Fields
payment_method.attachedpaymentMethodId, customerId, type
payment_method.detachedpaymentMethodId, type

Checkout Events

EventLogged Fields
checkout.session.completedsessionId, customerId, subscriptionId, mode, paymentStatus
checkout.session.expiredsessionId

Charge Events

EventLogged Fields
charge.succeededchargeId, customerId, amount, currency, paymentMethod
charge.failedchargeId, customerId, amount, failureCode, failureMessage (error)
charge.refundedchargeId, customerId, amountRefunded, refundCount

Helper Utilities

Import from @xenterprises/fastify-xstripe/helpers or via named export:

import { helpers } from "@xenterprises/fastify-xstripe";
HelperSignatureDescription
formatAmount(amount, currency)(number, string) => stringFormat Stripe amount to currency string, e.g. 2000, "USD""$20.00".
getPlanName(subscription)(sub) => stringGet the plan name from a subscription object.
isActiveSubscription(subscription)(sub) => booleanCheck if subscription status is "active" or "trialing".
isInTrial(subscription)(sub) => booleanCheck if subscription is in trial period.
getDaysUntilTrialEnd(subscription)(sub) => numberDays remaining in trial.
isRenewal(event)(event) => booleanCheck if an invoice event is a renewal.
calculateMRR(subscription)(sub) => numberCalculate MRR in cents.
getSubscriptionStatusText(status)(string) => stringHuman-readable status, e.g. "active""Active".
getEventDescription(event)(event) => stringHuman-readable event description.
getCustomerEmail(event, stripe)(event, stripe) => Promise<string>Resolve customer email from event.
isTestEvent(event)(event) => booleanCheck if event is from test mode.
getMetadata(event)(event) => objectExtract metadata from event.
getPaymentMethodType(paymentMethod)(pm) => stringHuman-readable payment method type, e.g. "Card".
getInvoiceLineItems(invoice)(invoice) => arrayGet line items from invoice.
isSubscriptionInvoice(invoice)(invoice) => booleanCheck if invoice is subscription-related.
getNextBillingDate(subscription)(sub) => DateGet next billing date as a Date object.
formatDate(timestamp)(number) => stringFormat Unix timestamp to readable date string.

Error Reference

All errors are plain Error objects whose messages name the plugin (xstripe), the option, and a usage example.

Startup Errors (thrown at registration)

ErrorCause
xstripe: missing required option `apiKey` (string), e.g. `app.register(xStripe, { apiKey: 'sk_test_...' })` apiKey option not provided.
xstripe: option `apiKey` must be a non-empty string, e.g. `app.register(xStripe, { apiKey: 'sk_test_...' })` apiKey is not a string or is empty.
xstripe: missing required option `webhookSecret` (string), e.g. `app.register(xStripe, { webhookSecret: 'whsec_...' })` webhookSecret option not provided.
xstripe: option `webhookSecret` must be a non-empty string, e.g. `app.register(xStripe, { webhookSecret: 'whsec_...' })` webhookSecret is not a string or is empty.
xstripe: option `webhookPath` must be a string starting with '/', e.g. `app.register(xStripe, { webhookPath: '/stripe/webhook' })` webhookPath is not a string starting with /.
xstripe: option `handlers` must be a plain object mapping event types to functions, e.g. `app.register(xStripe, { handlers: { 'invoice.paid': async (event, fastify, stripe) => {} } })` handlers is not a plain object, is an array, or contains non-function values.
xstripe: option `apiVersion` must be a non-empty string, e.g. `app.register(xStripe, { apiVersion: '2024-11-20.acacia' })` apiVersion is not a string or is empty.

Webhook Runtime Errors (HTTP 400)

ErrorCause
xstripe: missing stripe-signature headerWebhook request received without a stripe-signature header.
xstripe: webhook signature verification failed: <message>Invalid or tampered webhook signature. Unverified payloads are never dispatched to handlers.

Handler exceptions are logged and acknowledged with HTTP 200 ({ received: true, processed: false, error }) so Stripe does not immediately retry.

Environment Variables

The plugin never reads process.env itself. These variables are the consumer-side convention: your application reads them and passes the values into app.register(xStripe, { ... }).

VariableDescription
STRIPE_API_KEYStripe secret key (sk_test_... or sk_live_...). Pass as the required apiKey option.
STRIPE_WEBHOOK_SECRETWebhook signing secret from Stripe Dashboard or CLI (whsec_...). Pass as the required 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.xStripe. A POST route is registered at webhookPath inside an encapsulated context with a raw-body content parser (required for signature verification, so no extra Fastify configuration is needed). The route verifies the Stripe signature using stripe.webhooks.constructEvent() before any processing, 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.xStripe 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.xStripe (full Stripe SDK client — renamed from fastify.stripe in the pre-1.0 standardization)
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 — consumer reads env and passes values via options; the plugin never reads process.env
helper-exports: formatAmount, getPlanName, isActiveSubscription, isInTrial, getDaysUntilTrialEnd, calculateMRR, and more
Copyright © 2026