Stripe Plugin
Receive and verify Stripe webhooks with full type safety.
The @restaq/stripe plugin handles Stripe webhook verification and provides a fully-typed event catalog. The plugin requires the stripe npm package as a peer dependency so event types track your installed Stripe SDK version.
Installation
pnpm add @restaq/stripe stripenpm install @restaq/stripe stripeyarn add @restaq/stripe stripebun add @restaq/stripe stripeimport { stripe } from '@restaq/stripe';Setup
// relay.ts
import { restaq as createRestaq } from 'restaq';
import { stripe } from '@restaq/stripe';
export const restaq = createRestaq({
plugins: [
stripe(), // reads STRIPE_WEBHOOK_SECRET automatically
],
});- Open the Stripe Dashboard → Webhooks and add an endpoint pointing at
/api/webhook/stripeon your deployed app. - Copy the endpoint's Signing secret (starts with
whsec_) and set it asSTRIPE_WEBHOOK_SECRET.
Pass webhookSecret explicitly instead if you'd rather not rely on the env var:
stripe({ webhookSecret: 'whsec_...', toleranceSeconds: 300 }),Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| webhookSecret | string | process.env.STRIPE_WEBHOOK_SECRET | The endpoint's signing secret from the Stripe dashboard |
| toleranceSeconds | number | 300 | Max age of a signed timestamp before rejection |
Signature verification
The plugin verifies the stripe-signature header using HMAC-SHA256, per Stripe's own verification guide:
- Extracts timestamp
tand signaturev1from the header - Rejects if the timestamp is older than
toleranceSeconds - Computes HMAC-SHA256 of
${timestamp}.${rawBody}with your webhook secret - Compares signatures using constant-time comparison to prevent timing attacks
Event types
Events are namespaced as stripe.<stripe-event-type> (e.g., stripe.charge.succeeded). The plugin ships a fully-typed event catalog of ~250 Stripe events, taken from the full list of event types Stripe publishes. Register handlers in relay.handlers.ts, not relay.ts — see Organizing handlers:
// relay.handlers.ts
import type { AppRelay } from './relay';
export function registerHandlers(relay: AppRelay): void {
relay.on('stripe.charge.succeeded', async (event, ctx) => {
// event.data.object is a typed Stripe.Charge
const charge = event.data.object;
ctx.log.info('charge succeeded', { amount: charge.amount });
});
}Deduplication
Every Stripe event carries its own permanent event ID (evt_...). The plugin uses that as the Restaq event ID, so a redelivered event (Stripe retries on non-2xx responses) collapses into the same execution instead of running your handler twice.
Testing locally
Use relay trigger to simulate a signed Stripe webhook:
relay trigger stripe charge.succeeded --data '{"id":"ch_1234","amount":1000,"currency":"usd"}'This sends a properly signed POST request to /api/webhook/stripe with:
- A valid
stripe-signatureheader - A realistic payload shape that mimics Stripe's actual webhooks
Make sure STRIPE_WEBHOOK_SECRET is set to the same value on both relay dev and relay trigger, or unset on both — see the Local Development gotcha for why.
For end-to-end testing against real Stripe events (not simulated ones), use the Stripe CLI's stripe listen --forward-to localhost:3000/api/webhook/stripe.