Skip to content

Cell - Stripe Component ​

Add @celljs/stripe and stripe (the official SDK, a peer dependency) to the dependencies to use Stripe: @Autowired(StripeClient) gives the Stripe client of the configuration, and a webhook endpoint verifies the Stripe-Signature of events and routes them to your handlers by type. The API itself is the official SDK's, with its types.

bash
bun add @celljs/stripe stripe
yaml
# cell.yml: configuration is server-only unless a schema marks it public; take secrets from the environment
stripe:
  apiKey: ${env.STRIPE_SECRET_KEY}
  webhook:
    secret: ${env.STRIPE_WEBHOOK_SECRET}     # whsec_...
ts
import { Autowired } from '@celljs/core';
import { Body, Controller, Post } from '@celljs/mvc/lib/node';
import { StripeClient, StripeWebhookHandler } from '@celljs/stripe';
import type Stripe from 'stripe';

@Controller('checkout')
export class CheckoutController {
    @Autowired(StripeClient)
    protected readonly stripe: StripeClient;

    @Post()
    async create(@Body() order: { id: string }) {
        const session = await this.stripe.checkout.sessions.create({
            mode: 'payment',
            line_items: [{ price: 'price_123', quantity: 1 }],
            client_reference_id: order.id,
            success_url: 'https://shop.example.com/paid',
        });
        return { url: session.url };
    }
}

@StripeWebhookHandler('checkout.session.completed')
export class CheckoutCompletedHandler implements StripeWebhookHandler<Stripe.CheckoutSessionCompletedEvent> {
    async handle(event: Stripe.CheckoutSessionCompletedEvent) {
        // event.data.object is the session; mark the order paid once: Stripe can deliver an event more than once.
    }
}

Webhooks ​

The endpoint is POST /stripe/webhook (stripe.webhook.url, under server.path): register its URL in the Stripe Dashboard, or forward events to it during development with stripe listen --forward-to localhost:3000/stripe/webhook (set secret to the one it prints). It runs before the body parser and authentication, verifies the signature over the body as sent with each secret (a list serves several endpoints or a rotation) and a tolerance of 300 seconds, then runs the @StripeWebhookHandler components of the event type, then those declared without types, one after the other. It answers:

  • 200 {"received":true} when they all succeed (or when no handler takes the type: logged as a warning);
  • 400 when the signature does not match;
  • 500 when a handler throws (or the secret is not configured): Stripe retries the event for up to three days.

StripeWebhookVerifier verifies a request received on a route of your own: verify(rawBody, signatureHeader).

Configuration ​

yaml
stripe:
  apiKey: ${env.STRIPE_SECRET_KEY}
  config:                      # options of the Stripe client
    maxNetworkRetries: 2
    # apiVersion: 2025-...     # pin one; the SDK's own by default
  webhook:
    enabled: true
    url: /stripe/webhook
    secret: ${env.STRIPE_WEBHOOK_SECRET}   # or a list
    tolerance: 300             # seconds

The client is created on first use; rebind StripeFactory (@Component({ id: StripeFactory, rebind: true })) to create it another way, for example with a key per tenant. The SDK picks its fetch and SubtleCrypto build on Bun, Deno and Cloudflare Workers. Current versions of stripe need Node.js 20 or later (the rest of Cell runs on 18).

Testing without a Stripe account ​

stripe-mock answers every API of Stripe's OpenAPI spec and validates the parameters (it keeps no state). Point the client at it with config: { host: '127.0.0.1', port: 12111, protocol: 'http' } and any sk_test_<letters and digits> key; this package's own tests against it run with:

bash
docker run --rm -p 12111:12111 stripe/stripe-mock
STRIPE_MOCK_URL=http://127.0.0.1:12111 bunx turbo run test --filter=@celljs/stripe

Errors ​

Stripe's API errors are the SDK's (Stripe.errors.StripeError). Configuration and webhook failures are CellStripeErrors with a stable code and a hint:

CodeWhen
CELL_STRIPE_NOT_CONFIGUREDstripe.apiKey is not set when the client is injected
CELL_STRIPE_WEBHOOK_NOT_CONFIGUREDAn event arrived and stripe.webhook.secret is not set
CELL_STRIPE_SIGNATURE_INVALIDNo secret verifies the Stripe-Signature, or its timestamp is outside tolerance