← All @molecule/* packages · App templates
@molecule/api-payments-stripeProvider bond · payments · API (Node) · v1.2.2 · Apache-2.0
Stripe payment provider for molecule.dev.
npm install @molecule/api-payments-stripenpm · Source on GitHub · Implements @molecule/api-payments
@molecule/api-payments-stripe is a provider bond on the API (Node) side: it implements the payments core interface (@molecule/api-payments) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-bond, @molecule/api-i18n, @molecule/api-secrets
Secrets: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Stripe payment provider for molecule.dev.
provider
npm install @molecule/api-payments-stripe @molecule/api-bond @molecule/api-i18n @molecule/api-jwt @molecule/api-payments @molecule/api-proxy-agent @molecule/api-secrets stripe
AccountStatusConnected-account onboarding / payout eligibility status.
interface AccountStatus {
/** Stripe connected account ID. */
id: string
/** Whether the account can accept charges. */
chargesEnabled: boolean
/** Whether the account can receive payouts. */
payoutsEnabled: boolean
/** Whether there are any currently-due requirements (Stripe is blocked on something). */
requirementsCurrent: boolean
/** Stripe connected-account type (`standard`, `express`, `custom`), if known. */
type?: ConnectedAccountType
/** Currently-due requirement IDs from Stripe (empty when nothing is due). */
currentlyDue: readonly string[]
}
CheckoutSessionResultResult of creating or retrieving a checkout session.
interface CheckoutSessionResult {
id: string
url: string | null
/** The Stripe Subscription ID created by the checkout session, if available. */
subscription?: string
}
ConnectedAccountBusinessProfileSubset of Stripe business_profile fields commonly set during marketplace
onboarding.
interface ConnectedAccountBusinessProfile {
name?: string
url?: string
productDescription?: string
supportEmail?: string
supportPhone?: string
mcc?: string
}
ConnectWebhookEventNormalized Connect webhook event.
Provider-agnostic shape so consumers don't have to import Stripe types to dispatch on event kind.
interface ConnectWebhookEvent {
/** Recognized Connect event type, or `unknown` for unrelated events. */
type: ConnectWebhookEventType
/** Original Stripe event type string (e.g. `account.updated`). */
rawType: string
/** The ID of the primary resource this event is about (account, payout, transfer, fee). */
resourceId?: string
/** The connected account this event applies to, if Stripe sent one. */
accountId?: string
/** Raw event-data object (plain JSON shape — never the live Stripe object). */
data: Record<string, unknown>
}
CreateAccountLinkParamsParameters for creating an account link.
interface CreateAccountLinkParams {
/** Stripe connected account ID (`acct_...`). */
accountId: string
/** URL Stripe redirects the user back to after onboarding completes. */
returnUrl: string
/** URL Stripe redirects the user to if the link expires before completion. */
refreshUrl: string
/** Whether this is a first-time onboarding link or an update link. */
type: AccountLinkType
/** Optional idempotency key for safe retries. */
idempotencyKey?: string
}
CreateAccountLinkResultResult of creating an account link.
interface CreateAccountLinkResult {
/** Hosted onboarding URL the connected account holder should open. */
url: string
/** Unix timestamp (seconds) when the link expires. */
expiresAt: number
}
CreateConnectedAccountParamsParameters for creating a connected account.
interface CreateConnectedAccountParams {
/** Stripe account type — `standard`, `express`, or `custom`. */
type: ConnectedAccountType
/** Two-letter ISO country code for the account holder (e.g. `US`, `GB`). */
country: string
/** Email address of the account holder. */
email: string
/** Optional business profile fields (display name, website, MCC, etc.). */
businessProfile?: ConnectedAccountBusinessProfile
/** Optional metadata to attach to the connected account. */
metadata?: Record<string, string>
/** Optional idempotency key for safe retries. */
idempotencyKey?: string
}
CreateConnectedAccountResultResult of creating a connected account.
interface CreateConnectedAccountResult {
/** Stripe connected account ID (`acct_...`). */
id: string
/** Optional onboarding URL (only present when an account link is created in the same flow). */
accountLinkUrl?: string
}
CreatePayoutParamsParameters for creating a payout from a connected account's Stripe balance to its bank account.
interface CreatePayoutParams {
/** Connected account ID to issue the payout from (`acct_...`). */
accountId: string
/** Amount in the smallest currency unit (e.g. cents for USD). */
amount: number
/** Three-letter ISO currency code, lowercase (e.g. `usd`). */
currency: string
/** Optional payout method — `standard` or `instant`. */
method?: 'standard' | 'instant'
/** Optional metadata. */
metadata?: Record<string, string>
/** Optional idempotency key for safe retries. */
idempotencyKey?: string
}
CreatePayoutResultResult of creating a payout.
interface CreatePayoutResult {
/** Stripe payout ID (`po_...`). */
id: string
/** Amount paid out (smallest currency unit). */
amount: number
/** Three-letter ISO currency code. */
currency: string
/** Payout status (e.g. `pending`, `paid`, `failed`). */
status: string
/** Estimated arrival date (Unix timestamp in seconds). */
arrivalDate: number
}
CreateTransferParamsParameters for creating a transfer to a connected account.
interface CreateTransferParams {
/** Amount in the smallest currency unit (e.g. cents for USD). */
amount: number
/** Three-letter ISO currency code, lowercase (e.g. `usd`). */
currency: string
/** Destination connected account ID (`acct_...`). */
destination: string
/** Optional source charge to attach the transfer to (for separate-charges-and-transfers flow). */
sourceTransaction?: string
/** Optional transfer group string (groups related transfers/charges together). */
transferGroup?: string
/** Optional metadata. */
metadata?: Record<string, string>
/** Optional idempotency key for safe retries. */
idempotencyKey?: string
}
CreateTransferResultResult of creating a transfer.
interface CreateTransferResult {
/** Stripe transfer ID (`tr_...`). */
id: string
/** Amount transferred (smallest currency unit). */
amount: number
/** Three-letter ISO currency code. */
currency: string
/** Destination connected account ID. */
destination: string
/** Transfer group, if set. */
transferGroup?: string
}
NormalizedPurchaseNormalized purchase information (for one-time purchases).
interface NormalizedPurchase {
/**
* The payment provider.
*/
provider: PaymentProviderName
/**
* The purchase/transaction ID.
*/
purchaseId: string
/**
* The product ID.
*/
productId: string
/**
* Whether the purchase is valid.
*/
isValid: boolean
/**
* When the purchase was made (Unix timestamp in ms).
*/
purchaseDate: number
/**
* Raw data from the provider.
*/
rawData: unknown
}
NormalizedSubscriptionNormalized subscription information.
Use this interface to abstract away provider-specific differences.
interface NormalizedSubscription {
/**
* The payment provider.
*/
provider: PaymentProviderName
/**
* The subscription ID from the provider.
*/
subscriptionId: string
/**
* The product/plan ID.
*/
productId: string
/**
* Current subscription status.
*/
status: SubscriptionStatus
/**
* Whether the subscription is currently active.
*/
isActive: boolean
/**
* When the current period started (Unix timestamp in ms).
*/
currentPeriodStart?: number
/**
* When the current period ends (Unix timestamp in ms).
*/
currentPeriodEnd?: number
/**
* Whether the subscription will auto-renew.
*/
willRenew?: boolean
/**
* When the subscription was canceled (if applicable).
*/
canceledAt?: number
/**
* Raw data from the provider.
*/
rawData: unknown
}
SubscriptionResultNormalized subscription data from Stripe.
interface SubscriptionResult {
id: string
status: string
/**
* The Stripe Customer ID (`cus_...`) that owns this subscription. Used to bind
* a verified subscription to the calling user so a foreign subscription id
* cannot be claimed (ownership check in `verifyPayment`).
*/
customer?: string
/**
* The subscription's line items.
*
* `quantity` is the UNITS billed on that line — seats, on a per-seat plan.
* Carried because an app that sells seats has no other way to learn how many
* the customer actually paid for: the price id says what a seat costs, never
* how many were bought, so without this the app can only assume one.
*/
items: {
data: Array<{ id: string; quantity?: number; price?: { id?: string; product?: string } }>
}
current_period_start: number
current_period_end: number
cancel_at_period_end: boolean
canceled_at: number | null
}
SubscriptionUpdateParamsParameters for updating a subscription.
interface SubscriptionUpdateParams {
items?: Array<{
id: string
price: string
/**
* Units to bill — seats, on a per-seat plan. Stripe keeps the existing
* quantity for any field an update omits, so a caller changing plans must
* state it: leaving it off bills a new flat-priced plan at the old plan's
* seat count.
*/
quantity?: number
}>
cancel_at_period_end?: boolean
}
WebhookEventResultResult of verifying a webhook event.
interface WebhookEventResult {
type: string
data: { object: Record<string, unknown> }
}
AccountLinkTypeAccount-link types — onboarding (first-time) vs update (returning).
type AccountLinkType = 'account_onboarding' | 'account_update'
ConnectedAccountTypeConnected account type.
Stripe distinguishes three account types with different onboarding / dashboard responsibilities. See https://stripe.com/docs/connect/accounts.
type ConnectedAccountType = 'standard' | 'express' | 'custom'
ConnectWebhookEventTypeConnect webhook event types this provider knows how to interpret.
unknown is returned for any other Stripe event so callers can fall
through to the standard subscription webhook handler if needed.
type ConnectWebhookEventType =
'account.updated' | 'payout.created' | 'transfer.created' | 'application_fee.refunded' | 'unknown'
PaymentProviderPayment provider bond interface.
Each payment provider implements the methods relevant to its platform. All methods are optional since different platforms use different flows.
type PaymentProvider = PaymentProviderInterface
SubscriptionStatusSubscription status across providers.
type SubscriptionStatus =
'active' | 'canceled' | 'expired' | 'past_due' | 'trialing' | 'paused' | 'pending' | 'unknown'
cancelSubscription(subscriptionId)Immediately cancels a Stripe subscription.
function cancelSubscription(subscriptionId: string): Promise<SubscriptionResult>
subscriptionId — The Stripe subscription ID to cancel.Returns: The canceled subscription result.
chargeOffSession(options)Charges a customer's saved card OFF-SESSION for a fixed amount — the PREPAID top-up primitive (the customer is not present to authenticate).
Distinct from {@link reportUsageOverage} in the property that matters:
reportUsageOverage records an amount to be collected LATER on an invoice,
so the money is a receivable until the cycle closes. This CAPTURES the money
now and reports whether it actually settled, which is what makes a prepaid
balance prepaid. Only credit a balance on status: 'succeeded'.
A saved card is required: the charge uses the customer's invoice_settings. default_payment_method, falling back to their most recent attached card.
With off_session: true + confirm: true, Stripe either settles the payment
or fails — it never returns a client-side flow the absent customer could
complete. A card that demands 3DS therefore surfaces as
status: 'requires_action', which callers MUST treat as a failure and
resolve by asking the customer to re-authenticate on-session.
IDEMPOTENCY: the caller MUST pass a stable idempotencyKey. A retry inside
Stripe's 24h idempotency window returns the ORIGINAL PaymentIntent rather
than charging again — the property that makes an auto-refill retry safe.
This function performs NO gating of its own — it charges what it is told to. Whether a top-up is permitted (balance low? refill cap? velocity?) is the consuming application's decision.
function chargeOffSession(options: {
customerId: string
amountCents: number
currency?: string
paymentMethodId?: string
description?: string
metadata?: Record<string, string>
idempotencyKey: string
}): Promise<{
id: string | null
status: 'succeeded' | 'requires_action' | 'failed'
amountCents: number
declineCode?: string
failureMessage?: string
}>
options — Off-session charge options.options.customerId — The Stripe customer to charge (cus_...).options.amountCents — The amount to capture in cents (must be > 0).options.currency — ISO currency (defaults to usd).options.paymentMethodId — Explicit payment method; defaults to the customer's default card, then their most recently attached card.options.description — Human-readable statement/line description.options.metadata — Reconciliation metadata (e.g. userId, period).options.idempotencyKey — REQUIRED stable key (see IDEMPOTENCY above).Returns: The PaymentIntent id, the normalized outcome, the amount actually captured (0 unless settled), and a decline code/reason when it did not.
createAccountLink(params)Creates a Stripe account link the connected account holder uses to finish onboarding (or to update payout details).
function createAccountLink(params: CreateAccountLinkParams): Promise<CreateAccountLinkResult>
params — Account-link creation parameters.Returns: The hosted onboarding/update URL and its expiry (Unix timestamp, seconds).
createCheckoutSession(options)Creates a Stripe Checkout session for a new subscription.
function createCheckoutSession(options: {
priceId: string
quantity?: number
successUrl: string
cancelUrl: string
customerId?: string
clientReferenceId?: string
metadata?: Record<string, string>
idempotencyKey?: string
}): Promise<CheckoutSessionResult>
options — Checkout configuration.options.priceId — The Stripe Price ID for the subscription line item.options.successUrl — URL to redirect to after successful payment.options.cancelUrl — URL to redirect to if the user cancels.options.customerId — Optional existing Stripe Customer ID.options.clientReferenceId — Your own id for the buyer (the app's user id). Stripe echoes it back as client_reference_id on the session and on every checkout.session.* webhook event, which is how the customer (cus_…) created by this checkout gets linked to the account that paid. Always pass it: without it the first purchase has nothing tying the new Stripe customer to a user.options.metadata — Optional key-value metadata to attach to the session AND to the subscription it creates. Both, deliberately: see below.options.idempotencyKey — Optional idempotency key for safe request retries.options.quantity — Units of priceId to bill — seats, on a per-seat plan. Defaults to 1, which is right for every flat-priced plan. Clamped to a whole number >= 1: Stripe rejects 0 and fractions, and a caller that computed a seat count from a bad read must not turn that into a free subscription.Returns: The checkout session ID and URL.
createConnectedAccount(params)Creates a Stripe connected account for a marketplace seller / driver / provider.
function createConnectedAccount(
params: CreateConnectedAccountParams,
): Promise<CreateConnectedAccountResult>
params — Connected-account creation parameters.Returns: The new account ID.
createPayout(params)Issues a payout from a connected account's Stripe balance to its bank account.
Uses Stripe's Stripe-Account header to scope the call to the connected account.
function createPayout(params: CreatePayoutParams): Promise<CreatePayoutResult>
params — Payout parameters.Returns: The created payout.
createPortalSession(options)Creates a Stripe Billing Portal session so the user can manage their subscription (update payment method, cancel, view invoices) in Stripe's hosted portal.
function createPortalSession(options: {
customerId: string
returnUrl?: string
}): Promise<{ id: string; url: string } | null>
options — Portal configuration.options.customerId — The Stripe Customer ID to open the portal for.options.returnUrl — URL Stripe returns the user to when they exit the portal. Falls back to APP_ORIGIN/ORIGIN when omitted.Returns: The portal session ID and URL, or null when Stripe rejects the request (e.g. unknown customer).
createSetupIntent(options)Creates a Stripe SetupIntent for the saved-card flow.
If customerId is not provided, a new Stripe customer is created and its
ID is returned alongside the SetupIntent so the resource layer can persist
the customer ID for future SetupIntents and detachments.
function createSetupIntent(options: {
customerId?: string
metadata?: Record<string, string>
idempotencyKey?: string
}): Promise<{ id: string; clientSecret: string; customerId: string }>
options — SetupIntent creation options.options.customerId — Optional existing Stripe customer ID (cus_...).options.metadata — Optional metadata to attach to the SetupIntent.options.idempotencyKey — Optional idempotency key for safe retries.Returns: The SetupIntent ID, client secret, and customer ID.
createTransfer(params)Transfers funds from the platform balance to a connected account.
function createTransfer(params: CreateTransferParams): Promise<CreateTransferResult>
params — Transfer parameters.Returns: The created transfer.
detachPaymentMethod(paymentMethodId)Detaches a saved Stripe payment method from its customer.
function detachPaymentMethod(paymentMethodId: string): Promise<boolean>
paymentMethodId — The Stripe payment method ID (pm_...).Returns: true if Stripe acknowledged the detach, false otherwise.
getAccountStatus(accountId)Looks up a connected account's onboarding / payout status.
function getAccountStatus(accountId: string): Promise<AccountStatus>
accountId — Stripe connected account ID (acct_...).Returns: Normalized account status.
getCheckoutSession(sessionId)Retrieves a Stripe Checkout session by ID, including the associated subscription.
function getCheckoutSession(sessionId: string): Promise<CheckoutSessionResult>
sessionId — The Stripe Checkout session ID.Returns: The session ID, URL, and subscription ID (if a subscription was created).
getClient()Returns the lazily-initialized Stripe client. Throws if STRIPE_SECRET_KEY is not set.
function getClient(): Stripe
Returns: The shared Stripe SDK instance.
getSubscription(subscriptionId)Retrieves a Stripe subscription by ID with expanded item data.
function getSubscription(subscriptionId: string): Promise<SubscriptionResult>
subscriptionId — The Stripe subscription ID.Returns: The normalized subscription result.
normalizeSubscription(subscription)Normalizes a Stripe-specific SubscriptionResult to the common
NormalizedSubscription interface used across all payment providers.
function normalizeSubscription(subscription: SubscriptionResult): NormalizedSubscription
subscription — The Stripe subscription result to normalize.Returns: A NormalizedSubscription with provider-agnostic fields.
normalizeSubscriptionStatus(rawStatus)Maps a raw Stripe subscription status string (e.g. past_due, incomplete)
to the provider-agnostic SubscriptionStatus.
Shared between normalizeSubscription (verify path) and the webhook adapter so
both paths derive status identically.
function normalizeSubscriptionStatus(rawStatus: string | undefined): SubscriptionStatus
rawStatus — The raw Stripe status string, or undefined if absent.Returns: The normalized SubscriptionStatus ('unknown' for unrecognized/missing).
processConnectWebhook(headers, body)Verifies and normalizes a Stripe Connect webhook event.
Reuses the same STRIPE_WEBHOOK_SECRET env var as the standard webhook
pipeline (and the same signature verification logic) — Connect events
arrive on the same webhook endpoint when the platform's webhook is
configured to receive Connect events.
function processConnectWebhook(
headers: Record<string, string | string[] | undefined>,
body: string | Buffer<ArrayBufferLike>,
): ConnectWebhookEvent
headers — Request headers (looks up stripe-signature).body — The raw request body (string or Buffer).Returns: The verified, normalized Connect webhook event.
reportUsageOverage(options)Reports a usage-based OVERAGE charge to Stripe as a one-off invoice item against an existing customer (and, when given, attached to the open invoice of a specific subscription so it lands on the next cycle invoice).
This is the supported, type-safe path on the installed Stripe SDK (v22):
the legacy subscriptionItems.createUsageRecord API was removed in favor of
metered-price meter events / invoice items. A positive-amount invoice item
is the simplest cost-plus overage mechanism — Stripe aggregates open invoice
items and bills them at the customer's cycle close, so repeated incremental
calls accrete onto the same upcoming invoice.
IDEMPOTENCY: the caller MUST pass a stable idempotencyKey derived from
(user, period, amount) so a retry or a double-run within the same Stripe
idempotency window (24h) is collapsed to a single invoice item and never
double-charges (broker safety invariant 4).
This function performs NO gating of its own — it charges whatever it is told to. The decision of WHETHER to charge (configured? opted-in? paid? over budget?) lives entirely in the molecule-dev billing module, which is the single inert/opt-in gate (safety invariants 1 + 2).
function reportUsageOverage(options: {
customerId: string
amountCents: number
currency?: string
priceId: string
subscriptionId?: string
description?: string
metadata?: Record<string, string>
idempotencyKey: string
}): Promise<{ id: string; amountCents: number }>
options — Overage reporting options.options.customerId — The Stripe customer to bill (cus_...).options.amountCents — The overage amount in cents (must be > 0).options.currency — ISO currency (defaults to usd).options.priceId — The metered/overage Price id this reports against; recorded in metadata for reconciliation (the invoice item carries an explicit amount, so the price's unit amount is not used here).options.subscriptionId — Optional subscription to attach the item to so it bills on that subscription's cycle invoice.options.description — Human-readable line description.options.metadata — Extra reconciliation metadata (e.g. period).options.idempotencyKey — REQUIRED stable key (see IDEMPOTENCY above).Returns: The created invoice item id + the amount actually reported.
retrievePaymentMethod(paymentMethodId)Retrieves a saved Stripe payment method (card) and returns normalized metadata.
function retrievePaymentMethod(
paymentMethodId: string,
): Promise<{ id: string; brand: string; last4: string; expMonth: number; expYear: number } | null>
paymentMethodId — The Stripe payment method ID (pm_...).Returns: Brand, last4, and expiry, or null if the lookup fails.
updateSubscription(subscriptionId, params)Updates a Stripe subscription (e.g. changes plan, sets cancel_at_period_end).
function updateSubscription(
subscriptionId: string,
params: SubscriptionUpdateParams,
): Promise<SubscriptionResult>
subscriptionId — The Stripe subscription ID to update.params — The Stripe subscription update parameters.Returns: The updated subscription result.
verifyWebhookSignature(payload, signature)Verifies a Stripe webhook signature and parses the event payload.
Requires STRIPE_WEBHOOK_SECRET env var.
function verifyWebhookSignature(
payload: string | Buffer<ArrayBufferLike>,
signature: string,
): WebhookEventResult
payload — The raw request body (string or Buffer).signature — The stripe-signature header value.Returns: The verified webhook event with type and data.
paymentProviderPaymentProvider-compatible adapter for Stripe.
Wraps the Stripe SDK functions into a PaymentProvider-compatible object
for use with the molecule bond system. Supports subscription verification,
webhook handling, plan upgrades/downgrades, and cancellation.
const paymentProvider: PaymentProviderInterface
stripeSecretDefinitionsSecret definitions required by the Stripe payments bond.
const stripeSecretDefinitions: SecretDefinition[]
Implements @molecule/api-payments interface.
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { paymentProvider } from '@molecule/api-payments-stripe'
export function setupPaymentsStripe(): void {
bond('payments', 'stripe', paymentProvider)
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-i18n ^1.0.1@molecule/api-payments ^1.1.0@molecule/api-jwt ^1.0.1@molecule/api-secrets ^1.0.1STRIPE_SECRET_KEY (required) — Stripe secret key
sk_test_...STRIPE_WEBHOOK_SECRET (required) — Stripe webhook signing secret
whsec_...@molecule/api-bond@molecule/api-i18n@molecule/api-jwt@molecule/api-payments@molecule/api-proxy-agent@molecule/api-secretsstripeBond this as the payments provider so @molecule/api-payments's verifySubscription (and
the payment resource) work server-side — don't call the Stripe SDK directly for
verification. Env: STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET are SERVER-ONLY; only the
publishable key (pk_…) is client-side.
You do NOT need molecule's Express app, the bond() wiring, or the UI packages to use
this — the functions below are framework-agnostic. On a non-Express / non-molecule host
(Next.js App Router, serverless functions, Hono, Fastify), import them and call them from
your OWN route handlers:
import { createCheckoutSession, verifyWebhookSignature, getSubscription } from '@molecule/api-payments-stripe'.
They cover the whole flow — {@link createCheckoutSession} (server-owned priceId, so you
never take a price/amount from the client), {@link createPortalSession} (hosted Billing
Portal for payment-method updates/cancellation/invoices), {@link verifyWebhookSignature},
and the subscription getters/updaters — and carry the security contract (config-not-configured
errors, normalized status) for free, so reach for these instead of hand-rolling raw
stripe calls. In a Next.js App Router route, read the RAW webhook body with
await req.text() and the header with req.headers.get('stripe-signature'), then
verifyWebhookSignature(rawBody, signature) (the express.raw(...) note below is the
Express-host equivalent). Only @molecule/api-middleware-billing-routes (Express glue) and
@molecule/app-billing-react (molecule UI) are framework-coupled — skip THOSE on such a
host, but still use these bond functions underneath.
Two things a weak Stripe integration gets wrong:
constructEvent) hashes the exact bytes, so a parsed-then-re-serialized body
ALWAYS fails. In a molecule app you need NO special middleware: the always-included
@molecule/api-middleware-body-parser-express already captures the unparsed body as
req.rawBody (a string) on EVERY request — just pass req.rawBody + the stripe-signature
header to {@link verifyWebhookSignature}. Do NOT add a route-specific express.raw(...) here —
it is redundant and fights the global JSON parser (which has already consumed the stream and
set req.rawBody). (ONLY on a NON-molecule Express host that lacks req.rawBody do you mount
express.raw({ type: 'application/json' }) before the JSON parser; on Next.js App Router read
the raw body with await req.text().) NEVER act on an unverified webhook body — it is
attacker-controlled.event.id can arrive twice. Dedupe on it (the payment record's
UNIQUE(platformKey, transactionId) already blocks a double-grant), and return 2xx once
handled so Stripe stops retrying.Create checkout with SERVER-configured price ids ({@link createCheckoutSession}) — never an amount sent by the client.
Checkout returns the buyer to the APP, and the session must name the buyer.
paymentProvider.updateSubscription builds success_url /cancel_url with
resolveCheckoutRedirectUrls from @molecule/api-payments, so Stripe returns to
APP_ORIGIN/plan-updated?provider=stripe&sessionId={CHECKOUT_SESSION_ID} — the page
then calls POST /users/:id/verify-payment/stripe with that id from an origin whose
cookies authenticate. Returning to a separate API host instead sends a top-level
redirect with NO credentials, so the callback answers 401 and the paid plan is never
granted. It also sets client_reference_id + metadata.userId to the app's user id;
they are how a webhook links the newly created cus_… to the account that paid on a
FIRST purchase, and without them nothing ever writes the customer id — so any
usage/metered billing keyed on it can never charge. Pass clientReferenceId when you
call {@link createCheckoutSession} yourself.
A missing STRIPE_SECRET_KEY is NOT the same as "no active subscription."
getClient() throws a tagged config-not-configured error; verifySubscription,
updateSubscription, and cancelSubscription on {@link paymentProvider} detect
that tag (isConfigNotConfiguredError from @molecule/api-payments) and
RETHROW it instead of swallowing it into the same null / { updated: false }
/ false a genuine verification/update failure returns — so a caller (or its
own catch block) can tell "the operator forgot to set the secret" apart from
"this subscription/card is invalid" and surface the actionable 503 instead of
a generic 400/500.
Runs behind an outbound proxy when HTTPS_PROXY is set. The stripe SDK
builds its own agent and reads no proxy variable, so on a host whose only
egress path is a proxy every call used to fail with a bare connection error.
This bond now passes a CONNECT-capable agent into the SDK's httpAgent hook
(@molecule/api-proxy-agent, resolved against https://api.stripe.com so
NO_PROXY is honoured). With no proxy configured nothing is passed and the
SDK keeps its own default agent — a standalone app behaves exactly as before.
Allowlist api.stripe.com on the proxy.
Integration checklist — drive the real UI (live preview, no mocks; use the provider's TEST mode — test cards/sandbox accounts, never a live charge), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
read_activity tool (filter type 'webhook'); never
mock the event or modify production code to fake an entitlement.Translation strings are provided by @molecule/api-locales-payments-stripe.