@beignet/provider-webhooks-stripe
Stripe webhook verifier provider for Beignet applications.
This package adapts Stripe's official webhook signature verification to
@beignet/core/webhooks. Use it when an app receives Stripe events outside the
payments port, or when you want your billing webhook definition to use the
generic Beignet webhook route adapter.
For standard Beignet billing flows, prefer
@beignet/provider-payments-stripe with createPaymentWebhookRoute(...).
That path installs ctx.ports.payments, verifies through the payments port,
and is what beignet make payments generates.
Install
bun add @beignet/provider-webhooks-stripe stripe @beignet/coreConfigure
Set the endpoint signing secret for the webhook endpoint you are receiving:
STRIPE_WEBHOOK_SECRET=whsec_...
Dashboard endpoint secrets, live-mode secrets, and local Stripe CLI secrets are different values. For local testing, run:
stripe listen --forward-to http://localhost:3000/api/webhooks/stripeThen set STRIPE_WEBHOOK_SECRET to the whsec_... printed by the Stripe CLI
for that local app process.
beignet doctor --strict checks STRIPE_WEBHOOK_SECRET when this package is
installed. The package is optional by registration metadata because verifier
packages are wired at the route/server boundary instead of installed in
server/providers.ts; remove the dependency if the app is not using a generic
Stripe webhook endpoint.
Installed ports
This package does not install Beignet lifecycle ports. It exports
createStripeWebhookVerifier(...), which you pass to createWebhookRoute(...)
or verifyWebhook(...) at the route/server boundary.
It does not expose a provider escape hatch; pass a custom Stripe client when the app needs provider-specific SDK configuration.
Instrumentation
This verifier package does not record provider instrumentation directly.
createWebhookRoute(...) owns the route response, and your handler should
record app-specific audit entries, jobs, outbox messages, or custom devtools
events after verification when those side effects matter.
Define a webhook catalog
Keep the event catalog near the feature that owns the workflow:
// features/integrations/webhooks.ts
import { defineWebhook } from "@beignet/core/webhooks";
import { z } from "zod";
type StripeWebhookPayload = {
id: string;
object: string;
};
const stripeEventSchema = z.custom<StripeWebhookPayload>(
(value): value is StripeWebhookPayload =>
typeof value === "object" &&
value !== null &&
typeof (value as { id?: unknown }).id === "string" &&
typeof (value as { object?: unknown }).object === "string",
);
export const stripeWebhook = defineWebhook("integrations.stripe", {
provider: "stripe",
events: {
"customer.created": stripeEventSchema,
"charge.dispute.created": stripeEventSchema,
},
});The verified Beignet event has provider: "stripe" and uses the Stripe
event.data.object as event.payload. The complete Stripe event stays
available as event.raw, and account, livemode, request, and API version
details are copied into event.metadata.
Expose a Next.js route
Use the generic Beignet webhook route adapter so the raw body is preserved for Stripe signature verification. Wire the provider verifier at the route/server boundary so feature webhook catalogs stay provider-runtime free:
// app/api/webhooks/stripe/route.ts
import { createWebhookRoute } from "@beignet/next";
import { createStripeWebhookVerifier } from "@beignet/provider-webhooks-stripe";
import { stripeWebhook } from "@/features/integrations/webhooks";
import { handleStripeWebhookUseCase } from "@/features/integrations/use-cases";
import { env } from "@/lib/env";
import { server } from "@/server";
export const runtime = "nodejs";
const stripeWebhookVerifier = createStripeWebhookVerifier({
secret: () => env.STRIPE_WEBHOOK_SECRET,
});
export const { POST } = createWebhookRoute({
server,
webhook: stripeWebhook,
verify: ({ input }) => stripeWebhookVerifier.verify(input),
handle: async ({ ctx, event }) => {
await handleStripeWebhookUseCase.run({
ctx,
input: event.payload,
});
return {
status: 200,
body: { received: true },
};
},
});Handlers should key idempotency on event.id, update app-owned state inside a
transaction, and push durable follow-up work through outbox, jobs, listeners, or
notifications after verification.
Generic webhook routes acknowledge verified event types that are not listed in
the catalog by default. Set allowUnknownEvents: false only when unregistered
Stripe event types should be treated as endpoint misconfiguration and returned
as a 400.
Custom clients
Pass an existing Stripe client when your app already owns SDK configuration:
import Stripe from "stripe";
import { createStripeWebhookVerifier } from "@beignet/provider-webhooks-stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "");
export const verifier = createStripeWebhookVerifier({
client: stripe,
secret: () => process.env.STRIPE_WEBHOOK_SECRET,
});You can also set a custom signature header or Stripe timestamp tolerance:
createStripeWebhookVerifier({
secret: () => process.env.STRIPE_WEBHOOK_SECRET,
signatureHeader: "stripe-signature",
tolerance: 300,
});Relationship to payments
@beignet/provider-payments-stripe adapts Stripe to Beignet's PaymentsPort
and includes payment-specific operations such as checkout sessions, billing
portal sessions, refunds, and payment webhook conversion.
Use this package for the generic inbound webhook primitive. It does not create
checkout sessions, manage subscriptions, or install ctx.ports.payments.
Failure behavior
- Missing or invalid Stripe signatures throw
WebhookVerificationErrorvalues thatcreateWebhookRoute(...)maps to 400 responses. - Handler failures return 500 from
createWebhookRoute(...)so Stripe can retry. - Verified duplicate events should be handled idempotently by app code.
- Long-running work should be recorded through Beignet jobs, outbox delivery, listeners, or notifications instead of blocking the route.
Local and tests
Use a fake verifier for use-case tests and test createWebhookRoute(...) with
signed requests when verification behavior matters. Use the Stripe CLI for
local end-to-end delivery tests and remember that CLI endpoint secrets differ
from dashboard endpoint secrets.
Deployment notes
Use endpoint-specific whsec_... secrets per deployed route. Prefer
@beignet/provider-payments-stripe and createPaymentWebhookRoute(...) for
standard billing flows; use this generic verifier for non-payment Stripe event
catalogs.