Skip to main content

Payment Flows for SaaS

Time: ~10 minutes | Difficulty: Intermediate Payment flows are TagadaPay’s routing engine. For SaaS, they solve the problem Stripe cannot: run multiple processors/TPAs behind one vaulted card, with automatic cascade and failover.
Tested in production with TPA tpa_fb15b76112a8 (live Adyen-backed) and sandbox processor cascade. See mini-saas-billing.

The problem with Stripe

Customer card → Stripe only
                  ↓ decline → lost sale
                  ↓ Stripe bans account → all cards gone

The TagadaPay model

Customer card → vaulted once (payment instrument)

         ┌── Payment Flow ──┐
         │ sandbox (dev)    │  ← test cards
         │ → TPA (live)     │  ← your TagadaPay account
         │ → Stripe         │  ← optional direct processor
         │ → Checkout.com   │  ← optional
         └──────────────────┘

         Rebill every month — same vault, any processor
If a TPA is banned: update the payment flow to route through a different TPA or processor. Customers never re-enter their card.

Create a cascade flow (backend)

This is the flow created by npm run seed in the SaaS demo — tested and working:
import Tagada from '@tagadapay/node-sdk';

const tagada = new Tagada(process.env.TAGADA_API_KEY!);

const { processors } = await tagada.processors.list();
const sandbox = processors.find((p) => p.type === 'sandbox');
const router = processors.find((p) => p.type === 'tagadapay-router');

const flow = await tagada.paymentFlows.create({
  data: {
    name: 'SaaS Billing Flow',
    strategy: 'cascade',
    fallbackMode: true,
    maxFallbackRetries: 2,
    threeDsEnabled: true,
    stickyProcessorEnabled: true,
    pickProcessorStrategy: 'weighted',
    processorConfigs: [
      { processorId: sandbox!.id, weight: 100, disabled: false, nonStickable: false },
    ],
    fallbackProcessorConfigs: [
      { processorId: router!.id, orderIndex: 0 },
    ],
  },
});

console.log(flow.id);
// flow_35f3975175fe — verified in production
SettingWhat it does for SaaS
strategy: 'cascade'Try primary processor; on decline, fall through to next
stickyProcessorEnabled: trueReturning customers reuse their last successful processor
threeDsEnabled: trueSCA for EU rebills
fallbackProcessorConfigsOrdered list of backup processors/TPAs
About threeDsEnabled: this toggles whether 3DS is requested — how it runs is decided by the processor the flow routes to. With hosted-3DS PSPs (Adyen, Stripe, TagadaPay TPAs) the charge simply returns requireAction: 'redirect' and the processor hosts the challenge; you never create a 3DS session yourself. Standalone 3DS sessions (threeds.createSession) exist only for gateway-style processors like NMI and are rarely needed — see Quick Start.

Charge through a flow

Pass paymentFlowId on every charge — first payment and rebills:
const { payment } = await tagada.payments.process({
  amount: 1900,
  currency: 'EUR',
  storeId: 'store_xxx',
  paymentInstrumentId: 'inst_xxx',
  customerId: 'cus_xxx',
  initiatedBy: 'customer',
  paymentFlowId: 'flow_35f3975175fe',
});

console.log(payment.status);    // 'succeeded'
console.log(payment.processorId); // which processor won the route
Verified output from production test:
Payment: pay_666f3a317d1b succeeded processor: processor_4ae47011dc96

Add more processors to increase approval rate

Connect Stripe, Checkout.com, or Airwallex in the dashboard, then add them to the flow:
await tagada.paymentFlows.update({
  flowId: 'flow_35f3975175fe',
  data: {
    strategy: 'cascade',
    fallbackMode: true,
    maxFallbackRetries: 3,
    processorConfigs: [
      { processorId: 'processor_sandbox', weight: 50, disabled: false, nonStickable: false },
      { processorId: 'processor_stripe', weight: 50, disabled: false, nonStickable: false },
    ],
    fallbackProcessorConfigs: [
      { processorId: 'processor_tagadapay_router', orderIndex: 0 },
      { processorId: 'processor_checkout_com', orderIndex: 1 },
    ],
  },
});
No frontend changes. No re-vaulting. The same paymentInstrumentId works across all processors.

Switch TPA without re-collecting cards

When your live TPA (tpa_xxx) has issues:
  1. Provision a new TPA (or connect a direct Stripe/Adyen processor)
  2. Add it to the payment flow’s fallbackProcessorConfigs
  3. Optionally disable the old processor
  4. Existing subscriptions rebill through the new route automatically
For subscription-level processor migration, use:
await tagada.subscriptions.changeProcessor({
  subscriptionIds: ['sub_xyz'],
  processorId: 'processor_new',
});
See Subscriptions & Rebilling.

Inspect your flow at runtime

const flow = await tagada.paymentFlows.retrieveWithProcessors('flow_35f3975175fe');

console.log(flow.name);              // 'SaaS Billing Flow'
console.log(flow.strategy);          // 'cascade'
console.log(flow.processorConfigs);  // primary processors
console.log(flow.fallbackProcessorConfigs); // fallback chain
The SaaS demo exposes this at GET /api/payment-flow so your frontend can display the active routing config.

Strategies reference

StrategyBest for SaaS
simpleSingle processor — dev/testing
cascadeRecommended — try primary, fall through on decline
Weighted + stickySplit traffic across processors, stick returning customers
Full multi-PSP guide: Multi-PSP Routing & Vault.