Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

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

PropType
secretKey?string

Secret or restricted API key (sk_… / rk_…). Sent as a Bearer credential.

Typestring
managedPayments?boolean

Sells every checkout through Managed Payments, Stripe’s merchant-of-record mode. Requires an account Stripe approved for it.

Typeboolean
apiVersion?string

Overrides the pinned Stripe-Version. Response shapes may no longer match the SDK types.

Typestring
baseUrl?string

Used verbatim; defaults to https://api.stripe.com.

Typestring
fetch?typeof fetch

Custom fetch implementation.

Typetypeof fetch

Authentication

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. licenseKeys is false and all four client.licenseKeys methods throw unsupported — 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 }) throws unsupported (checkoutCustomAmount: false).
  • Pause is immediate only. subscriptions.pause({ behavior: 'period_end' }) throws unsupported — scheduling a pause would require Subscription Schedules — and pauseAtPeriodEnd is always false.
  • 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 400 maps to RevenueError { code: 'validation' }.
  • managedPayments needs 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.

  • refundStatus is 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 reports status: 'paid'.
  • Drafts are dropped client-side, because Stripe’s status filter takes a single value. A page can be short or empty and still have a cursor — a renewal sits as a draft for about an hour before Stripe charges it.
  • createdAt is the invoice creation time, not the payment time — that lives on status_transitions.paid_at in raw.
  • getInvoiceUrl throws not_found before finalization. The URLs are minted at finalization and expire 30 days after the due date (capped at 120); an expired PDF link answers 400.

Provider notes

  • Price.checkoutRef is the price ID, not the product ID. Stripe checkout line items take price_…; Product.id is the prod_… and must never be passed to checkouts.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 apiVersion only if you know the shapes still line up.
  • A 2xx on usage.report is not a confirmation. Meter events are validated synchronously but processed asynchronously: an unknown customer, or an eventName with no matching meter, is dropped silently. The only signals are the v1.billing.meter.error_report_triggered and v1.billing.meter.no_meter_found thin 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_id and value are only the meter’s default keys, so a meter with custom keys needs them passed through metadata. A usage timestamp may 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.update merges metadata instead of replacing it. Stripe merges metadata key 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 is complete but with payment_status: unpaid is reported as open.
  • 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’s raw for 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-After on 429. Stripe signals retryability with Stripe-Should-Retry, which the adapter maps onto RevenueError.retryable. retryAfter is therefore usually undefined, so the client’s bounded retry falls back to a one-second wait — and skips the retry entirely when the header says false. Anything longer is your own back-off.

Last updated on August 9, 2026

Was this page helpful?