Restaq
Plugins

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 stripe
import { 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
  ],
});
  1. Open the Stripe Dashboard → Webhooks and add an endpoint pointing at /api/webhook/stripe on your deployed app.
  2. Copy the endpoint's Signing secret (starts with whsec_) and set it as STRIPE_WEBHOOK_SECRET.

Pass webhookSecret explicitly instead if you'd rather not rely on the env var:

stripe({ webhookSecret: 'whsec_...', toleranceSeconds: 300 }),

Configuration

OptionTypeDefaultDescription
webhookSecretstringprocess.env.STRIPE_WEBHOOK_SECRETThe endpoint's signing secret from the Stripe dashboard
toleranceSecondsnumber300Max 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:

  1. Extracts timestamp t and signature v1 from the header
  2. Rejects if the timestamp is older than toleranceSeconds
  3. Computes HMAC-SHA256 of ${timestamp}.${rawBody} with your webhook secret
  4. 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-signature header
  • 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.

On this page