@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 injectsStripeClient, 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.
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".
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.
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 tolocalhost and mints the signing secret that tunnel uses.
1
Install the CLI
2
Pair the CLI with your account
.env.3
Forward events to your app
Point the tunnel at your webhook route. The skeleton serves on port The command prints the signing secret for this session:
8030 under the api prefix.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.Restart the app so
.env.yml
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:
Receiving webhooks in a controller
Signature verification needs the exact bytes Stripe sent, andcontext.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.
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.
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-controlledmetadata 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:
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:
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 throwsPaymentException from @talosjs/payment (extending Exception, mapped to InternalServerError) with a machine-readable key.
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.