Stripe
Configure the Stripe provider — secret key, pinned API version, test mode, webhooks, capabilities, and quirks.
Stripe is a payment processor (you remain the merchant of record). 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.
stringapiVersion?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.
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.
Webhooks
Create the endpoint in Developers → Webhooks and copy the signing secret (whsec_…).
import { parseWebhookEvent, verifyWebhook } from 'revenue-sdk/stripe';
const headers = request.headers;
const body = await request.text();
if (!(await verifyWebhook({ headers, body, secret: env.STRIPE_WEBHOOK_SECRET }))) {
return new Response('invalid signature', { status: 401 });
}
const event = await parseWebhookEvent({ headers, body });
The Stripe-Signature header carries a timestamp (t=) and one or more signatures. Only v1=
signatures are trusted — v0= is a deliberately fake test scheme and is ignored — and deliveries older
than 300 seconds are rejected. The whsec_ secret is used verbatim; never strip or base64-decode
it.
Events worth subscribing to: customer.subscription.created, customer.subscription.updated,
customer.subscription.deleted, customer.subscription.paused, customer.subscription.resumed,
checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid.
Capabilities
| Capability | Value |
|---|---|
cancellationReason |
true |
checkoutStatus |
true |
checkoutSuccessUrl |
true |
endTrial |
true |
hostedCheckout |
true |
listSubscriptionsByCustomer |
true |
portalReturnUrl |
true |
prorationBehaviors |
['invoice_now', 'none', 'prorate'] |
revoke |
true |
uncancel |
true |
Quirks
Price.checkoutRefis the price ID, not the product ID. Stripe checkout line items takeprice_….Product.idis theprod_…— never pass it 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. - Form encoding is invisible to you. Stripe’s API takes
application/x-www-form-urlencodedwith bracket notation, not JSON. The SDK’s encoder handles sequential array indices, omittingundefined, sendingnullas'', booleans as'true'/'false', andDateas unix seconds. You pass plain objects and never see it. status=allis always sent on subscription lists. Without it Stripe silently hides canceled subscriptions, so a canceled subscription would simply vanish from your list.- Plan changes send the current item ID. Omitting
items[0][id]adds the new price instead of replacing the old one — silent double-billing. The SDK fetches the subscription first and sends the item ID, so this can’t happen throughsubscriptions.changePlan. current_period_start/current_period_endlive on subscription items, not on the subscription. The SDK reads them from the first item and exposes them ascurrentPeriodStart/currentPeriodEnd.cancelAtPeriodEndneeds two fields. In flexible billing mode — Stripe’s default for new integrations — a portal cancellation sets onlycancel_atand leavescancel_at_period_endatfalse. The SDK checkscancel_at_period_end || cancel_at !== null, anduncancelclears both.checkout.status === 'complete'means paid. A session that iscompletebutpayment_status: unpaidis reported asopen.- The email filter is case-sensitive and emails are not unique — see Customers & portal.
- No
Retry-Afteron 429. Stripe signals retryability withStripe-Should-Retry, soRevenueError.retryAfteris usuallyundefinedand the client’s bounded retry does not engage. Use your own back-off. - Timestamps are unix seconds on the wire and are converted to
Dateeverywhere in the normalized models.