X Enterprises
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

  1. Reads the raw request body and stripe-signature header.
  2. Calls stripe.webhooks.constructEvent(rawBody, sig, webhookSecret) — returns HTTP 400 on failure.
  3. Looks up handlers[event.type]. User handlers override defaults via object spread.
  4. Calls the matched handler with (event, fastify, stripe).
  5. Always returns HTTP 200, even if the handler throws — Stripe interprets non-200 as a retry signal.

Request / Response

Request headers:

HeaderRequiredDescription
stripe-signatureYesHMAC 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

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)
Copyright © 2026