Skip to content

Getting started

PayFanout is a unified multi-PSP payment abstraction for React + TypeScript. You get one consistent API and one set of embedded UI components over multiple Payment Service Providers, your application code never knows which PSP is active. PayFanout is provider-agnostic: implement any payment gateway by writing a new adapter package only, zero changes to core, zero changes to consuming application code, verified by a shared conformance suite.

PayFanout is stateless. It has no database and persists nothing.

The consuming application owns:

  • the mapping between its internal payment/order ids and pspPaymentIds,
  • the webhook event dedupe store ("have I processed this event id"),
  • any audit / event log.

PayFanout's job is orchestration + normalization only. If you are looking for where PayFanout stores a payment, it doesn't. See examples/demo/server.mts for what the host app is expected to keep.

Two non-negotiables baked into the design

  • We never store card data, and neither do you. Card capture happens exclusively in Stripe's Payment Element / Paysafe.js hosted iframe fields, both SAQ-A eligible; there is no raw card <input> anywhere. Saved cards / recurring payments change nothing about this: the PSP vaults the card and hands back an opaque token, your database stores that token exactly like it stores a pspPaymentId, never a PAN.
  • No redirect to a hosted payment page for cards. Fields render embedded in your UI, styled by your design tokens. 3DS/SCA challenges run inline (iframe/modal). Genuinely redirect/voucher payment methods (iDEAL, PaysafeCard, Skrill…) are modeled honestly via the flow capability field, never forced into an embedded illusion.

The packages

PackageRunsPurpose
@payfanout/coreanywhereUnified domain model, adapter contracts, currency + error + refund-state helpers. Zero dependencies, zero PSP code.
@payfanout/serverserverPaymentService over an adapter registry + framework-agnostic webhook handlers.
@payfanout/reactclient<PayFanoutProvider>, usePayFanout, <PaymentFields>, <PayButton>.
@payfanout/adapter-stripe-serverserverStripe Node SDK: PaymentIntents, refunds, webhook verification. Pins an explicit apiVersion.
@payfanout/adapter-stripeclientStripe.js + Payment Element.
@payfanout/adapter-paysafe-serverserverPaysafe Payments REST API. Edge-runtime compatible (WebCrypto, no Node builtins).
@payfanout/adapter-paysafeclientPaysafe.js hosted iframe fields (tokenize-first).
@payfanout/conformancetestsThe contract suite every adapter, present or future, must pass.

Client packages have zero dependency on anything holding secrets; this is enforced mechanically by scripts/check-boundaries.mjs (part of pnpm run check), not by convention. Every adapter config requires an explicit environment: "sandbox" | "live", never inferred from key prefixes.

Mental model in 30 seconds

  1. Server creates a payment session through PaymentService, naming the PSP per call. Amounts are always integer minor units at this boundary.
  2. The session's clientSecret (Stripe) or signed context (Paysafe) goes to the browser.
  3. React mounts <PaymentFields> + <PayButton>, the customer pays without your code ever touching card data.
  4. Webhooks arrive as one normalized UnifiedWebhookEvent, whichever PSP sent them.

Next steps

Versioning note

Packages version independently (see .changeset/config.json) and don't share one library-wide release number. Where docs say "v1", read "the current feature scope", not any package's semver.