Skip to main content
@talosjs/payment-stripe wraps the Stripe API behind eight injectable classes — StripeCheckoutSession, StripeWebhookEvent, StripeCustomer, StripeCustomerPortal, StripeProducts, StripeDiscount, StripeAnalytics, and the StripeProvider facade — plus typed models for sessions, webhook events, customers, products, prices, and discounts. Each class talks to Stripe through a shared StripeClient and normalizes snake_case responses into camelCase Talos types, converting Unix timestamps to Date objects, so call sites never touch the SDK directly.

Installation

@talosjs/payment-stripe depends on Stripe’s SDK.

Configuration

Every class injects StripeClient, which reads its credentials from AppEnv and constructs the underlying SDK once for the whole container.
StripeClient validates the secret key eagerly, so a missing one fails fast with PaymentException (TOKEN_REQUIRED) rather than on the first API call. If you need an escape hatch, StripeClient exposes the configured SDK instance through its sdk getter for endpoints the package doesn’t wrap.

Environment variables

Stripe expresses every monetary amount in the currency’s minor unit, so unitAmount: 2900 means $29.00. The package passes these values through unchanged.

Usage

Checkout sessions

StripeCheckoutSession creates and retrieves hosted checkout sessions. Line items reference existing Stripe price ids, and quantity defaults to 1.
mode accepts "payment", "subscription", or "setup". Passing customerId attaches the session to an existing Stripe customer and takes precedence over customerEmail. Both create() and get(id) return a CheckoutSessionType with url, status, paymentStatus, amountTotal, currency, and metadata.

Webhooks

StripeWebhookEvent.construct() verifies the signature against the raw request body and returns a discriminated union keyed on type, so narrowing on type gives you a fully typed data payload.
construct() needs the exact bytes Stripe sent. A controller cannot reach them, because the framework parses the JSON body before the controller runs — see Reading the raw body for what to do instead.
Five event types are mapped. Anything else throws PaymentException (UNSUPPORTED_EVENT_TYPE), so configure your Stripe endpoint to send only these:

Provider facade

StripeProvider implements IPaymentProvider, a three-method interface covering the checkout and webhook flow. Depend on it when a service only needs to start a checkout and read webhooks, and you want the Stripe specifics behind an interface.

Customers

StripeCustomer covers the customer lifecycle, including a structured billing address that maps to Stripe’s address fields.
list() returns { items, hasMore } and defaults to a limit of 10. Paginate by passing the last item’s id as startingAfter.
get(id) throws a plain Error when the customer has been deleted in Stripe, not a PaymentException.

Products and prices

StripeProducts manages both products and their prices. A product carries the descriptive metadata; a price carries the amount, currency, and recurrence.
type accepts "one_time" or "recurring"; interval ("day", "week", "month", "year") and intervalCount apply only to recurring prices, and intervalCount defaults to 1. remove(id) deletes the product, which Stripe rejects for products that already have prices attached — deactivate with update(id, { active: false }) instead.

Customer portal

StripeCustomerPortal creates a billing portal session where customers manage their own subscriptions and payment methods.

Discounts

StripeDiscount creates percentage or fixed-amount coupons. For type: "percentage", amount is the percentage; for type: "fixed", it is a minor-unit amount and currency defaults to "usd".
Passing code also creates a customer-facing promotion code on top of the coupon, inheriting maxRedemptions and redeemBy. Without it, the coupon can only be applied programmatically. durationInMonths is read only when duration is "repeating". update() only accepts name and metadata — Stripe coupons are otherwise immutable, so changing an amount or duration means creating a new one.

Analytics

StripeAnalytics.get() aggregates succeeded charges into daily periods over a date range and counts subscriptions by status.
The summary is computed from a single page of Stripe results — limit charges (default 100) and 100 subscriptions. Ranges busier than that are silently truncated, so treat the output as a dashboard figure, not an accounting total.
Passing currency filters charges to that currency; otherwise the reported currency is taken from the first charge in the range.

Local development

Stripe delivers webhooks to a public URL, so a local server never receives them on its own. The Stripe CLI opens a tunnel from your account to localhost and mints the signing secret that tunnel uses.
1

Install the CLI

2

Pair the CLI with your account

This opens a browser to authorize the CLI. It stores a restricted key locally and does not touch your .env.
3

Forward events to your app

Point the tunnel at your webhook route. The skeleton serves on port 8030 under the api prefix.
The command prints the signing secret for this session:
4

Set the secret

Copy that value into your local environment. It is specific to the tunnel and differs from the secret of a dashboard-registered endpoint.
.env.yml
Restart the app so AppEnv picks up the change.
5

Trigger an event

In a second terminal, make Stripe emit a real event through the tunnel.
stripe trigger creates the underlying objects in test mode, so the payload matches production shape. Run stripe trigger --help for the full list.

Forwarding only the mapped events

StripeWebhookEvent.construct() throws UNSUPPORTED_EVENT_TYPE on anything outside its five mapped types, and stripe trigger often emits several events at once. Restrict the tunnel to what the package handles:
Apply the same event list to the endpoint you register in the Stripe dashboard for staging and production.

Receiving webhooks in a controller

Signature verification needs the exact bytes Stripe sent, and context.request.native no longer holds them: for an application/json request the framework calls req.json() while building the context, so the body is consumed before the controller runs. Reading it again throws Body already used. Stripe serializes webhook payloads with two-space indentation, so re-serializing the parsed payload the same way reproduces the original bytes and the signature verifies.
The service calls StripeWebhookEvent.construct(rawBody, signature, secret) and dispatches on event.type. Returning 200 even on failure stops Stripe from retrying an event you will never accept, and keeps a forged request from learning whether its signature was close.
Log the failure rather than discarding it. Verification that breaks — a rotated STRIPE_WEBHOOK_SECRET, a payload shape that no longer round-trips — looks exactly like a quiet endpoint from Stripe’s side, since every delivery still answers 200. Alert on a sustained run of rejected events.
Answer Stripe quickly and do the work afterwards. Stripe treats a response slower than 20 seconds as a failure and retries, so acknowledge immediately and hand the event to a queue rather than processing it inline.

Verifying against the raw bytes

The controller above depends on Stripe’s payload formatting rather than on the bytes themselves. Round-tripping is exact for the payloads Stripe sends, but it is reconstruction, not the original: a payload whose keys are integer-like — user-controlled metadata is the realistic case — comes back reordered and fails verification. To remove that dependency, serve the webhook from a listener that never parses the body:
Forward to that port instead — stripe listen --forward-to localhost:8031/webhooks/stripe. The trade-off is that this route sits outside the framework, so it gets no middlewares, no route roles, and no request logging.

Inspecting traffic

stripe listen prints each event and the status your app returned. For the API calls behind them, tail the account log in a separate terminal:
Every object created while STRIPE_SECRET_KEY holds an sk_test_ key lives in test mode and is visible in the dashboard’s test view.

API

Exceptions

The package throws PaymentException from @talosjs/payment (extending Exception, mapped to InternalServerError) with a machine-readable key.
Errors raised by Stripe itself propagate as the SDK’s own error types.

Types

Beyond the service classes, @talosjs/payment-stripe exports the model backing them: IPaymentProvider, LineItemType, CheckoutSessionCreateType, CheckoutSessionType, the per-event *DataType and *EventType members and the WebhookEventType union, the customer types (StripeCustomerType, StripeCustomerCreateType, StripeCustomerUpdateType, StripeCustomerAddressType, StripeCustomerListOptionsType, StripeCustomerListResultType), the portal, product, price, discount, and analytics types, and the EStripeEvent, EStripeDiscountType, EStripeDiscountDuration, EStripePriceType, and EStripePriceInterval enums. Each enum has a matching string-literal alias (StripeEventType, StripeDiscountTypeType, StripePriceIntervalType, and so on) so payloads accept plain strings. For Polar as the payment backend instead, see Polar.