HazelJS LogoHazelJS
Introducing @hazeljs/payment: One API, Any Provider
HazelJS Blog

Introducing @hazeljs/payment: One API, Any Provider

Author
HazelJS Team
3/8/2026
General
← Back to Blog

Multi-provider payment integration for HazelJS — Stripe first, with one API for checkout, customers, subscriptions, and webhooks. Add PayPal or custom gateways without changing your code.

We're shipping @hazeljs/payment — a multi-provider payment package for HazelJS. Use Stripe today; plug in PayPal, Paddle, or your own gateway tomorrow, with one interface and no vendor lock-in.

Why a payment package?

Most apps need payments at some point: one-time purchases, subscriptions, or both. The usual approach is to wire the Stripe SDK (or PayPal, Paddle, etc.) directly into your controllers. That works, but when you want to support a second provider, run A/B tests, or switch gateways, you end up with if (provider === 'stripe') scattered everywhere and duplicated logic for checkout, customers, and webhooks.

We wanted a single API that works across providers. Same methods for creating checkout sessions, managing customers, listing subscriptions, and handling webhooks — so your business logic stays provider-agnostic and you can add or swap providers by configuration.

What's in the box

One API, many providers

  • PaymentServicecreateCheckoutSession(), createCustomer(), getCustomer(), listSubscriptions(), getCheckoutSession(), parseWebhookEvent(). Pass an optional provider name to use a specific gateway; otherwise the default provider is used.
  • PaymentModule — Register with PaymentModule.forRoot({ stripe: { secretKey, webhookSecret }, providers: { mygateway: myProvider }, defaultProvider: 'stripe' }). The convenience stripe option creates StripePaymentProvider for you; providers lets you add custom or third-party implementations.
  • PaymentController (optional) — POST /payment/checkout-session and POST /payment/webhook/:provider so you can create sessions and receive webhooks without writing route handlers yourself. Webhook routes require the raw request body for signature verification.

Stripe first

Stripe is the first built-in provider. Configure it with your secret key and webhook secret (or env vars), and you get:

  • Checkout sessions for one-time payments and subscriptions (including trials)
  • Customer create/retrieve
  • Subscription listing
  • Webhook verification and event parsing

You can still use the raw Stripe client when you need something provider-specific: paymentService.getProvider<StripePaymentProvider>('stripe').getClient().

Extensible

Implement the PaymentProvider interface (createCheckoutSession, createCustomer, getCustomer, listSubscriptions, getCheckoutSession, isWebhookConfigured, parseWebhookEvent), instantiate your provider, and register it in forRoot({ providers: { paypal: new MyPayPalProvider(config) } }). Your app code keeps calling paymentService.createCheckoutSession(options, 'paypal') — no branching on provider names in business logic.

Quick start (Stripe)

1. Install and register the module

npm install @hazeljs/payment
import { HazelApp } from '@hazeljs/core';
import { PaymentModule } from '@hazeljs/payment';

const app = new HazelApp({
  modules: [
    PaymentModule.forRoot({
      stripe: {
        secretKey: process.env.STRIPE_SECRET_KEY,
        webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
      },
    }),
  ],
});

2. Create a checkout session

const result = await paymentService.createCheckoutSession({
  successUrl: 'https://yourapp.com/success',
  cancelUrl: 'https://yourapp.com/cancel',
  customerEmail: 'user@example.com',
  clientReferenceId: userId,
  lineItems: [{
    priceData: {
      currency: 'usd',
      unitAmount: 1999,
      productData: { name: 'Premium Plan', description: 'Monthly access' },
    },
    quantity: 1,
  }],
});

// Redirect user to result.url

3. Subscriptions and customers — Use subscription: { priceId, quantity, trialPeriodDays } for subscription checkouts, and createCustomer() / getCustomer() to attach payments to users. Store the provider customer ID in your DB and pass it as customerId when creating sessions.

4. Webhooks — Point Stripe (or your provider) at POST /payment/webhook/stripe. Ensure the route receives the raw body. Then in your app:

const event = paymentService.parseWebhookEvent('stripe', req.rawBody, req.headers['stripe-signature']);
// Handle event.type: checkout.session.completed, customer.subscription.updated, etc.

Multiple providers in one app

You can register Stripe and one or more custom providers and choose per request:

PaymentModule.forRoot({
  defaultProvider: 'stripe',
  stripe: { secretKey: '...', webhookSecret: '...' },
  providers: {
    paypal: new MyPayPalProvider(paypalConfig),
  },
});

// Later:
await paymentService.createCheckoutSession(options, 'stripe');
await paymentService.createCheckoutSession(options, 'paypal');

Webhook URLs are per provider: /payment/webhook/stripe, /payment/webhook/paypal, etc.

When to use it

Good fit: SaaS billing, one-time purchases, subscriptions, and any app that wants a single payment API with the option to support multiple gateways or switch providers without rewriting business logic.

Docs: Payment package — full API, provider interface, and how to add your own gateway.

What's next

We'll keep improving the payment package based on feedback. If you need a second provider (e.g. PayPal) out of the box, open an issue or PR on GitHub. Until then, the PaymentProvider interface is small and well-defined so you can implement any gateway you need.

— The HazelJS Team