> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tagada.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment flows (cascade & weighted)

> Create and manage routing flows on behalf of a merchant: cascade failover and weighted splits across the merchant's TPAs.

# Payment flows for your merchants

A **payment flow** is the routing rule a charge runs through: which processors to
try, in what proportion, in what fallback order, and whether 3DS is enforced.
[Server-to-server payments](/developer-tools/partners/payments) explains how a
flow is *executed* at charge time. This page is about the other half —
**creating and managing flows on behalf of one of your merchants**, entirely
over the API.

<Check>
  **Yes — a partner can create payment flows for a merchant.** Verified end-to-end
  against production: a merchant-scoped key can `list`, `create`, `update` and
  `delete` flows for that merchant's account. The flow you create is owned by the
  merchant and routes across **that merchant's TPAs**.
</Check>

The direct-merchant guide
[Multi-PSP Routing & Vault](/developer-tools/node-sdk/multi-stripe-routing)
shows a merchant wiring flows across PSPs *they* connected. As a partner you do
the exact same thing, with one difference: each **processor in a partner flow is
one of the merchant's TPAs** (the Adyen sub-merchant, the Stripe MoR account,
etc.) that you provisioned for them.

***

## Which key can manage flows

The payment-flow endpoints are **account-scoped** and require `org:admin`. Use a
**merchant-scoped key**, not your raw partner key:

| Key                                                | Manages flows? | Why                                                                                                                      |
| -------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Processing Key** `tp_sk_…` (restricted to a TPA) | ✅              | Resolves to the TPA's merchant **account** as `org:admin`. The same key you charge with.                                 |
| **CRM Key** `sk_crm_…`                             | ✅              | Account-scoped, `org:admin`.                                                                                             |
| **Partner key** `tp_sk_…` (partner-scoped)         | ❌              | Spans many accounts — it can't resolve a *single* merchant for an account-scoped call. Mint a merchant-scoped key first. |

<Note>
  One merchant-scoped key is enough for **all of that merchant's TPAs**. A
  processing key restricted to one TPA still resolves to the merchant account, so a
  flow it creates can reference every TPA the merchant owns. You don't need a key
  per processor.
</Note>

Mint a merchant-scoped key with your partner key, then build a fresh client with it:

```ts theme={null}
import Tagada from '@tagadapay/node-sdk';

const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!); // tp_sk_… (partner-scoped)

// Option A — a CRM key for the merchant account
const crmKey = await partner.partners.crm.merchants.keys.create('acc_merchant42');
const merchant = new Tagada(crmKey.token);          // sk_crm_live_…

// Option B — reuse the Processing Key you already charge that merchant with
const merchant2 = new Tagada(process.env.MERCHANT_42_PROCESSING_KEY!); // tp_sk_…
```

See [API keys](/developer-tools/partners/api-keys) for the full minting flow.

***

## What's auto-created with a TPA

Provisioning a TPA doesn't leave you with nothing — but it doesn't fully wire a
chargeable store either. Here's exactly what exists, and when:

| Entity                                          | Auto-created? | When                                                                                                    |
| ----------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
| **Store** (`store_…`)                           | ✅ Yes         | At TPA **creation** — for convenience, so you have a `storeId` to charge. It is *not* bound to the TPA. |
| **Processor** (`tagadapay-router`)              | ✅ Yes         | When the TPA goes **`active`** — one router processor pointing at the TPA.                              |
| **Payment flow** (`"Default"`)                  | ✅ Yes         | When the TPA goes **`active`** — a `simple` flow with that one processor at `weight: 100`.              |
| **Store → flow link** (`selectedPaymentFlowId`) | ❌ **No**      | Never set automatically.                                                                                |

<Warning>
  **The store and the `Default` flow are not linked out of the box.** A charge by
  `storeId` alone will fail with `Payment flow ID not found` until you either:

  * pass `paymentFlowId` explicitly on `payments.process` (recommended — see Step 3), or
  * set the store's default flow in the
    [dashboard](https://app.tagadapay.com) → Store → Payments.

  This is intentional: one merchant can own several TPAs, so Tagada doesn't guess
  which one a store should default to — you decide, per charge or per store.
</Warning>

So the minimum to charge a freshly provisioned TPA is: **one `storeId` + one
`paymentFlowId`** (the auto-created `Default` flow is enough for a single TPA).
Create your own cascade/weighted flow only when you want routing across several
TPAs.

***

## Step 1 — Find the merchant's processors

Each TPA shows up as a processor of type `tagadapay-router`, and the TPA id lives
in `options.tagadapayAccountId`. That's how you map a TPA to its processor:

```ts theme={null}
const { processors } = await merchant.processors.list();

// The router processor for a specific TPA:
const tpaProcessor = processors.find(
  (p) => p.type === 'tagadapay-router' && p.options?.tagadapayAccountId === 'tpa_xxx',
);
console.log(tpaProcessor?.id); // 'processor_…' → use this in processorConfigs
```

`listWithProcessors` is the convenience variant — it returns every processor on
the account *plus* the existing flows in one call:

```ts theme={null}
const { flows, allProcessors } = await merchant.paymentFlows.listWithProcessors();

console.log(allProcessors);
// [
//   { id: 'processor_11737089bcd4', type: 'tagadapay-router', name: 'Adyen sub-merchant' },
//   { id: 'processor_c1a3f4309244', type: 'tagadapay-router', name: 'Stripe MoR' },
// ]
```

<Info>
  For partner-provisioned merchants the processor `type` is `tagadapay-router` —
  that's the TPA being routed to. (A merchant who connects their *own* Stripe/NMI
  would instead see `type: 'stripe'`, `'nmi'`, etc.)
</Info>

***

## Step 2 — Create a flow

### Cascade (failover)

Primary TPA first; if it declines (non-terminal), Tagada automatically retries
the same charge on the next TPA — no retry code on your side.

```ts theme={null}
const adyen  = 'processor_11737089bcd4';
const stripe = 'processor_c1a3f4309244';

const cascade = await merchant.paymentFlows.create({
  data: {
    name: 'Cascade — Adyen → Stripe',
    strategy: 'cascade',
    fallbackMode: true,
    maxFallbackRetries: 2,
    threeDsEnabled: true,
    stickyProcessorEnabled: true,
    pickProcessorStrategy: 'weighted',
    processorConfigs: [
      { processorId: adyen, weight: 100, disabled: false, nonStickable: false }, // primary
    ],
    fallbackProcessorConfigs: [
      { processorId: stripe, orderIndex: 0 }, // tried if Adyen declines
    ],
    abuseDetectionConfig: null,
  },
});

console.log(cascade.id); // 'flow_…'
```

### Weighted (traffic split)

Send a share of traffic to each TPA. Great for load-balancing or gradually
migrating a merchant onto a new acquirer.

```ts theme={null}
const weighted = await merchant.paymentFlows.create({
  data: {
    name: 'Weighted — Adyen 50 / Stripe 50',
    strategy: 'cascade',
    fallbackMode: false,
    maxFallbackRetries: 0,
    threeDsEnabled: true,
    stickyProcessorEnabled: false,
    pickProcessorStrategy: 'weighted',
    processorConfigs: [
      { processorId: adyen,  weight: 50, disabled: false, nonStickable: false },
      { processorId: stripe, weight: 50, disabled: false, nonStickable: false },
    ],
    fallbackProcessorConfigs: [],
    abuseDetectionConfig: null,
  },
});
```

### Field reference

| Field                      | What it does                                                                   |
| -------------------------- | ------------------------------------------------------------------------------ |
| `strategy`                 | `'simple'` (one processor) or `'cascade'` (routing + fallback).                |
| `fallbackMode`             | If `true`, a declined primary retries the `fallbackProcessorConfigs` in order. |
| `maxFallbackRetries`       | How many fallback TPAs to try before giving up.                                |
| `pickProcessorStrategy`    | `'weighted'`, `'lowestCapacity'`, or `'automatic'` (round-robin).              |
| `stickyProcessorEnabled`   | Returning customers reuse the TPA that last succeeded for them.                |
| `threeDsEnabled`           | Enforce 3DS when the issuer requires it.                                       |
| `processorConfigs`         | Primary TPAs + weights.                                                        |
| `fallbackProcessorConfigs` | Backup TPAs, tried in `orderIndex` order on decline.                           |

***

## Step 3 — Use the flow when charging

Pass `paymentFlowId` to `payments.process` to route a charge through a specific
flow. It overrides the store's default for that call:

```ts theme={null}
const { payment } = await merchant.payments.process({
  paymentInstrumentId,
  storeId: 'store_xxx',
  paymentFlowId: cascade.id, // ← this charge runs the cascade
  amount: 4999,
  currency: 'EUR',
  returnUrl,
});

payment.processorName;                       // final capturing TPA
payment.transactions?.map((t) => t.status);  // e.g. ['declined', 'captured']
```

<Tip>
  You can omit `paymentFlowId` **only if the store has a default flow set** (see the
  warning above — it isn't set automatically). Passing it explicitly is the
  reliable path and lets you keep several flows on one merchant (e.g. a standard
  cascade and a high-value flow) and pick per charge — exactly like the
  [direct-merchant guide](/developer-tools/node-sdk/multi-stripe-routing#multiple-flows-for-different-scenarios).
</Tip>

***

## Manage existing flows

```ts theme={null}
// List
const flows = await merchant.paymentFlows.list();

// Retrieve one (with processor details resolved)
const flow = await merchant.paymentFlows.retrieveWithProcessors('flow_xxx');

// Update — note: fallbackProcessorConfigs is always required on update
await merchant.paymentFlows.update({
  flowId: 'flow_xxx',
  data: {
    threeDsEnabled: true,
    processorConfigs: [
      { processorId: adyen,  weight: 70, disabled: false, nonStickable: false },
      { processorId: stripe, weight: 30, disabled: false, nonStickable: false },
    ],
    fallbackProcessorConfigs: [{ processorId: stripe, orderIndex: 0 }],
  },
});

// Delete (soft-delete; also clears the flow from any store using it as default)
await merchant.paymentFlows.del('flow_xxx');
```

***

## See it running

The cascade + weighted flows above are wired in the runnable demo — a picker for
single / cascade / weighted, plus a "force the primary to decline" toggle so you
can watch the failover happen:

<Card title="partners-examples/02-merchant-checkout" icon="github" href="https://github.com/TagadaPay/partners-examples/tree/main/02-merchant-checkout">
  One merchant, several TPAs, cascade and weighted routing — end to end.
</Card>
