One checkout.
Every market.
Zero vendor lock.
Reactive payment orchestration for modern web apps. The right provider, the right methods, the right flow — picked in the browser, the moment your app knows enough to decide. Headless, typed, MIT.
A checkout that knows what your app knows.
Every modern SDK lets you take a payment. Tenderlane decides which payment to take, in the browser, the moment context changes — with types that hold across providers, markets, and methods.
Adapts the second your app does.
When the customer changes country, currency, or cart — the checkout reroutes in the same render. Right provider. Right methods. Right flow. No reload. No server round-trip.
Your UI. Your stack. Any PSP.
Headless components, framework-agnostic core, swappable adapters. Start on Stripe. Add Polar for subscriptions. Drop in your local PSP. The router does not care.
Routing rules are data, not code.
A JSON shape with first-match-wins semantics. Edit them in a dashboard. Store them in a DB. Ship them from a feature flag. Catch the typo in "gpb" at compile time.
What checkout routing usually looks like.
Same requirement — "show TWINT in Switzerland, fall back to Stripe Checkout everywhere else." Left: how teams ship it today. Right: how it reads with Tenderlane.
const [provider, setProvider] = useState('stripe');
const [methods, setMethods] = useState(['card']);
const [flow, setFlow] = useState('checkout-session');
// re-run when country changes
useEffect(() => {
if (country === 'CH') {
setFlow('payment-intent');
setMethods(['card', 'twint']);
} else if (country === 'NL') { /* … */ }
}, [country]);
// re-run when currency changes
useEffect(() => { /* … */ }, [currency]);
// 14 more conditionals. Untyped. Sticky. - Untyped strings everywhere — typos surface in production
- Order depends on render timing, not declaration
- Races when context updates mid-fetch
- Rules live in code — ops can't A/B test or hot-patch
import { createRulesRouter } from '@tenderlane/core';
export const routing = createRulesRouter({
rules: [
{ id: 'ch-inline',
when: { country: 'CH', currency: 'chf' },
use: { provider: 'stripe', flow: 'payment-intent',
paymentMethods: ['card', 'twint'] } },
{ id: 'nl-ideal',
when: { country: 'NL' },
use: { paymentMethods: ['card', 'ideal', 'sepa'] } },
],
fallback: { flow: 'checkout-session',
paymentMethods: ['card'] },
});
// re-evaluates automatically when context changes.
// rules are JSON: store anywhere, edit anywhere. - Literal-typed end-to-end — country, currency, method, flow
- First-match-wins is the contract, not a side-effect
- Re-evaluates synchronously with context — no races
- Rules are JSON: A/B test them, store them, ship them from a flag
Four layers. Each one swappable. Each one MIT.
A core that knows nothing about your framework, a headless state machine on top, framework bindings, and provider adapters. TanStack-inspired — and the browser bundle pays only for what each route uses.
Types, rules router, auto router, middleware, errors, catalog, server handler. Zero runtime deps.
Framework-agnostic headless state machine. idle → ready → submitting → success.
React provider + hooks via useSyncExternalStore. Vue/Svelte planned.
Per-PSP. Stripe and Polar today. Browser SDK lazy-loaded only when the active route needs it.
* The browser bundle only loads provider SDKs after the active route resolves to that provider — pure redirect flows can ship without any PSP JS at all.
The whole integration, on screen.
One file. One provider. One rule. Render a button — get a charge. Everything else (A/B tests, custom adapters, server-side rules) composes on top of this exact shape.
import { TenderlaneProvider, useTenderlaneCheckout } from '@tenderlane/react';
import { createRulesRouter } from '@tenderlane/core';
import { stripeProvider } from '@tenderlane/stripe';
const stripe = stripeProvider({ publishableKey: 'pk_…', serverEndpoint: '/api/pay' });
const routing = createRulesRouter({
rules: [
{ id: 'ch-inline',
when: { country: 'CH', currency: 'chf' },
use: { provider: 'stripe',
flow: 'payment-intent',
paymentMethods: ['card', 'twint'] } },
],
fallback: { provider: 'stripe',
flow: 'checkout-session',
paymentMethods: ['card'] },
});
function Checkout() {
const checkout = useTenderlaneCheckout();
return <button disabled={!checkout.canSubmit} onClick={checkout.submit}>Pay</button>;
}
<TenderlaneProvider config={{ context, providers: [stripe], routing }}>
<Checkout />
</TenderlaneProvider> Honest scope. Pre-1.0.
We'd rather you know what's missing than discover it on launch day. This list reflects the alpha. The roadmap moves in public.
- Core types + rulesRouter + autoRouter
- React bindings via useSyncExternalStore
- Stripe adapter — Checkout + PaymentIntent
- Polar adapter — redirect flow
- Server handler — Web Request/Response
- Catalog primitive (inline · remote · PSP-sourced)
- Phantom-type provider metadata
- Polar iframe embedded flow
- Cross-PSP webhook verification helpers
- Subscriptions, refunds
- Catalog response caching
- lookup_key support for Stripe catalogs
- Stripe Elements (field-level)
- Vue / Solid / Svelte bindings
- Adyen, Paddle, LemonSqueezy, Revolut adapters
- Failover · ML routing · devtools
- First v1.0 stable promise
Don't request a demo.
Just read the code.
MIT. On npm. On GitHub. No sales call. No "talk to us." If you have an hour and a Stripe account, you can have a multi-region routed checkout running this afternoon.