Stripe
Configure the Stripe provider — secret key, pinned API version, test mode, webhooks, limitations, and the traps worth knowing.
Stripe is a payment processor — you remain the merchant of record, unless you opt into
Managed Payments with managedPayments. Import the factory from
revenue-sdk/stripe.
import { createClient } from 'revenue-sdk';
import { stripe } from 'revenue-sdk/stripe';
const client = createClient({
provider: stripe({ secretKey: process.env.STRIPE_SECRET_KEY! }),
});
Factory options
secretKey?string
Secret or restricted API key (sk_… / rk_…). Sent as a Bearer credential.
stringmanagedPayments?boolean
Sells every checkout through Managed Payments, Stripe’s merchant-of-record mode. Requires an account Stripe approved for it.
booleanapiVersion?string
Overrides the pinned Stripe-Version. Response shapes may no longer match the SDK types.
stringbaseUrl?string
Used verbatim; defaults to https://api.stripe.com.
stringfetch?typeof fetch
Custom fetch implementation.
typeof fetchAuthentication
Create a key in the Stripe dashboard under Developers → API keys. Either a secret key (sk_…) or a
restricted key (rk_…) works — a restricted key is the better default. Grant it read/write on
Products, Prices, Checkout Sessions, Customers, Subscriptions, and the Billing Portal.
Sandbox & test mode
Test mode is selected by the key prefix: sk_test_… / rk_test_… talk to test data, sk_live_… to
live data. There is no server option — swap the key via environment variables.
Limitations
- No license keys.
licenseKeysisfalseand all fourclient.licenseKeysmethods throwunsupported— Stripe has no license-key API, and Billing Entitlements is not a substitute (no key string, no activation count, no device identity, and no way for a shipped app to check its own license). See License keys. - No pay-what-you-want.
checkouts.create({ customAmount })throwsunsupported(checkoutCustomAmount: false). - Pause is immediate only.
subscriptions.pause({ behavior: 'period_end' })throwsunsupported— scheduling a pause would require Subscription Schedules — andpauseAtPeriodEndis alwaysfalse. - A checkout expiry must be 30 minutes to 24 hours out. The SDK deliberately does not enforce
Stripe’s window client-side: it is measured against Stripe’s clock, so a boundary value would be
rejected by clock skew alone. Stripe’s
400maps toRevenueError { code: 'validation' }. managedPaymentsneeds an approved account and narrows what Stripe accepts. Access is gated by an eligibility review (digital products only, no Connect platforms, supported business countries), every product needs an eligible tax code, and existing subscriptions cannot be migrated — only ones bought through a Managed Payments session. The SDK sends nothing Managed Payments rejects, so no client call changes shape, but Stripe Tax, Elements, and third-party tax integrations are unavailable to you.
Full values for every capability: capability matrix.
Webhooks
Create the endpoint in Developers → Webhooks and copy the signing secret (whsec_…). Verification
and parsing come from the subpath:
import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/stripe';
See Handle webhooks for the full handler.
Events worth subscribing to: customer.subscription.created, customer.subscription.updated,
customer.subscription.deleted, customer.subscription.paused, customer.subscription.resumed,
customer.subscription.pending_update_applied, customer.subscription.pending_update_expired,
checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid.
Stripe also documents distinct evt_ IDs describing the same change, so de-duplicating on
event.idempotencyKey stops redeliveries, not fan-out — see Webhooks.
Orders
See Orders for the model. A unified Order is a Stripe Invoice, not a
Charge: order.paid maps from invoice.paid, so event.order.id is an in_… and resolves through
orders.get.
refundStatusis never set. A refund is a Charge-level object and leaves no trace on the invoice — no flag, no amount — so a refunded invoice still reportsstatus: 'paid'.- Drafts are dropped client-side, because Stripe’s
statusfilter takes a single value. A page can be short or empty and still have a cursor — a renewal sits as adraftfor about an hour before Stripe charges it. createdAtis the invoice creation time, not the payment time — that lives onstatus_transitions.paid_atinraw.getInvoiceUrlthrowsnot_foundbefore finalization. The URLs are minted at finalization and expire 30 days after the due date (capped at 120); an expired PDF link answers400.
Provider notes
Price.checkoutRefis the price ID, not the product ID. Stripe checkout line items takeprice_…;Product.idis theprod_…and must never be passed tocheckouts.create.- The API version is pinned to a constant so response shapes match the SDK’s types regardless of
your account default. Override it with
apiVersiononly if you know the shapes still line up. - A
2xxonusage.reportis not a confirmation. Meter events are validated synchronously but processed asynchronously: an unknown customer, or aneventNamewith no matching meter, is dropped silently. The only signals are thev1.billing.meter.error_report_triggeredandv1.billing.meter.no_meter_foundthin webhooks, which the SDK does not parse — subscribe to them in the dashboard if silent revenue loss matters. - Meter payload keys are configurable.
stripe_customer_idandvalueare only the meter’s default keys, so a meter with custom keys needs them passed throughmetadata. A usagetimestampmay be at most 35 calendar days old and 5 minutes ahead, and the endpoint allows only one concurrent call per customer per meter — parallelize across customers, not within one. See Usage-based billing. customers.updatemerges metadata instead of replacing it. Stripe mergesmetadatakey by key, so a key you leave out keeps its stored value — the opposite of every other provider here.- The email filter is case-sensitive and emails are not unique — see Customers & portal.
checkout.status === 'complete'means paid. A session that iscompletebut withpayment_status: unpaidis reported asopen.- Managed Payments always applies Adaptive Pricing, so the buyer can be charged in a different
currency than the one on the
Price— read the session’srawfor what was actually charged. The legal seller on the receipt becomes Stripe’s entity, not your business. - A plan change on a subscription with no items throws
provider_error. Stripe needs the current item ID to replace a price instead of adding one; without it the change would double-bill, so the SDK refuses rather than send it. - No
Retry-Afteron 429. Stripe signals retryability withStripe-Should-Retry, which the adapter maps ontoRevenueError.retryable.retryAfteris therefore usuallyundefined, so the client’s bounded retry falls back to a one-second wait — and skips the retry entirely when the header saysfalse. Anything longer is your own back-off.