Skip to content

Stripe

@voltage/stripe registers a Stripe client as a global NestJS provider and handles webhook signature verification automatically via middleware. Verified events are emitted through @voltage/event-manager so any service can react to them with @OnEvent.

Terminal window
yarn add @voltage/stripe @voltage/event-manager @voltage/zod zod stripe
import { StripeModule } from '@voltage/stripe';
@Module({
imports: [
EventManagerModule.forRoot(),
StripeModule.register({
apiKey: process.env.STRIPE_SECRET_KEY,
}),
],
})
export class AppModule {}
import { StripeModule } from '@voltage/stripe';
StripeModule.registerAsync({
inject: [AppConfiguration],
useFactory: (config: AppConfiguration) => ({
apiKey: config.stripe.secretKey,
}),
});

The module is global — you do not need to import it in feature modules.

EventManagerModule must be imported before StripeModule. Webhook events are emitted during request handling, after bootstrap, so ordering is not critical — but it is a good habit.

Use @Inject(Stripe) to inject the client:

import { Inject, Injectable } from '@nestjs/common';
import Stripe from 'stripe';
@Injectable()
export class PaymentService {
constructor(@Inject(Stripe) private readonly stripe: Stripe) {}
async createPaymentIntent(amount: number, currency: string) {
return this.stripe.paymentIntents.create({ amount, currency });
}
}

The middleware intercepts requests at /stripe/event/snapshot and /stripe/event/thin before any NestJS guards, interceptors, or controllers run. Signature verification uses Stripe.webhooks.constructEvent — the route will never reach your application code unless the signature is valid.

Configure webhook secrets in the module options:

StripeModule.register({
apiKey: process.env.STRIPE_SECRET_KEY,
webhookSecrets: {
snapshot: process.env.STRIPE_WEBHOOK_SECRET,
thin: process.env.STRIPE_THIN_WEBHOOK_SECRET,
},
});

Both snapshot and thin are optional. The middleware only activates for the paths whose secret is configured.

Webhook verification requires the raw request body. Pass rawBody: true when creating your NestJS application:

const app = await NestFactory.create(AppModule, { rawBody: true });

Listen to StripeEvent with @OnEvent:

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@voltage/event-manager';
import { StripeEvent } from '@voltage/stripe';
@Injectable()
export class BillingService {
@OnEvent(StripeEvent)
async onStripeEvent({ event }: StripeEvent) {
switch (event.type) {
case 'payment_intent.succeeded':
await this.handlePayment(event.data.object);
break;
case 'customer.subscription.deleted':
await this.handleCancellation(event.data.object);
break;
}
}
}

event is the object returned by Stripe.webhooks.constructEvent — a fully typed Stripe.Event discriminated union. All handlers receive the same event regardless of which endpoint it arrived on (snapshot or thin).

The middleware uses softEmit, so a handler error is logged but does not affect other handlers or the HTTP response.

The Stripe CLI forwards live Stripe events to your local server and generates a temporary webhook secret.

Install the CLI and log in:

Terminal window
stripe login

Forward events to your local snapshot endpoint:

Terminal window
stripe listen --forward-to localhost:3000/stripe/event/snapshot

The CLI prints a webhook signing secret on startup:

> Ready! Your webhook signing secret is whsec_... (^C to quit)

Set that value as STRIPE_WEBHOOK_SECRET in your local environment. The secret changes each time stripe listen restarts.

Trigger a test event in a second terminal:

Terminal window
stripe trigger payment_intent.succeeded

Any event type from the Stripe events reference can be triggered this way. The CLI also supports forwarding connect events:

Terminal window
stripe listen --forward-to localhost:3000/stripe/event/snapshot \
--forward-connect-to localhost:3000/stripe/event/snapshot
interface StripeConfiguration {
/** Stripe secret key (sk_live_... or sk_test_...). */
apiKey: string;
/**
* Stripe API version. Defaults to the version bundled with the SDK.
* Pin this explicitly in production — upgrading the stripe package will otherwise
* silently change the API version and may alter the shape of webhook payloads.
*/
apiVersion?: string | null;
/** Number of automatic retries on network errors. Defaults to 1. */
maxNetworkRetries?: number;
/** Custom HTTP agent, e.g. for a proxy. */
httpAgent?: Agent | null;
/** Request timeout in milliseconds. Defaults to 80000. */
timeout?: number;
/** Stripe API host. Defaults to api.stripe.com. */
host?: string;
/** Stripe API port. Defaults to 443. */
port?: number;
/** Protocol to use. Defaults to https. */
protocol?: 'http' | 'https';
/** Whether to send telemetry to Stripe. Defaults to false. */
telemetry?: boolean;
/** Webhook signature verification. */
webhookSecrets?: {
/** Signing secret for the snapshot webhook endpoint (/stripe/event/snapshot). */
snapshot?: string;
/** Signing secret for the thin webhook endpoint (/stripe/event/thin). */
thin?: string;
/**
* Maximum allowed age of a webhook event in seconds.
* Applies to both snapshot and thin. Defaults to Stripe's default (300s).
*/
tolerance?: number;
};
}