> ## 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.

# SaaS Billing Quick Start

> Subscribe a customer in 15 minutes — core-js frontend + node-sdk backend

# SaaS Billing Quick Start

**Time**: \~15 minutes | **Difficulty**: Intermediate

This walkthrough matches the tested [`mini-saas-billing`](https://github.com/TagadaPay/examples/tree/main/mini-saas-billing) example. Every code block below runs in production.

***

## Prerequisites

* Node.js 18+
* A Tagada CRM API key (`sk_crm_live_…`) — [Get an API key](/developer-tools/node-sdk/get-api-key)
* A store (`store_xxx`)

***

## Step 1 — Seed your SaaS plan (backend)

Create a recurring product and a **cascade payment flow** (sandbox for dev → live TPA for production):

```bash theme={null}
cd mini-saas-billing
cp env.example .env
# TAGADA_API_KEY=sk_crm_live_...
# TAGADA_STORE_ID=store_...
npm install
npm run seed
```

The seed script creates:

| Resource     | Example ID          | Purpose                                     |
| ------------ | ------------------- | ------------------------------------------- |
| Payment flow | `flow_35f3975175fe` | Cascade: sandbox → `tagadapay-router` (TPA) |
| Product      | `product_…`         | SaaS Pro Plan                               |
| Price        | `price_…`           | €19.00/month recurring                      |

Verify everything works:

```bash theme={null}
npm run verify
# ✅ Payment succeeded
# ✅ Subscription active
# ✅ Rebill OK
```

***

## Step 2 — Backend API (node-sdk)

Your server holds the API key and exposes routes the frontend calls.

```ts title="server/index.ts" theme={null}
import Tagada from '@tagadapay/node-sdk';

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

// Vault card from browser tagadaToken
app.post('/api/payment-instruments', async (req, res) => {
  const { tagadaToken, storeId, customerData } = req.body;
  const result = await tagada.paymentInstruments.createFromToken({
    tagadaToken, storeId, customerData,
  });
  res.json(result);
});

// Charge through payment flow
app.post('/api/payments', async (req, res) => {
  const { amount, currency, storeId, paymentInstrumentId, customerId } = req.body;
  const result = await tagada.payments.process({
    amount, currency, storeId, paymentInstrumentId, customerId,
    initiatedBy: 'customer',
    paymentFlowId: process.env.TAGADA_PAYMENT_FLOW_ID,
  });
  res.json(result);
});

// Create subscription after first charge
app.post('/api/subscriptions', async (req, res) => {
  const { customerId, priceId, storeId, currency, defaultPaymentInstrumentId, paymentId } = req.body;
  const result = await tagada.subscriptions.create({
    customerId, priceId, storeId, currency,
    defaultPaymentInstrumentId, paymentId,
  });
  res.json(result);
});
```

<Warning>
  **Never expose `TAGADA_API_KEY` to the browser.** The frontend only sends `tagadaToken` to your backend.
</Warning>

***

## Step 3 — Frontend checkout (core-js)

The browser tokenizes the card and calls your API routes.

```tsx title="web/src/App.tsx" theme={null}
import { useCardTokenization } from '@tagadapay/core-js/react';

const { tokenizeCard } = useCardTokenization({ environment: 'production' });

async function subscribe() {
  // 1. FRONTEND — tokenize (PCI-safe)
  const { tagadaToken } = await tokenizeCard({
    cardNumber: '4242424242424242',
    expiryDate: '12/30',
    cvc: '123',
    cardholderName: 'Alex Founder',
  });

  // 2. BACKEND — create customer
  const { customer } = await fetch('/api/customers', {
    method: 'POST',
    body: JSON.stringify({ storeId, email, firstName, lastName }),
  }).then((r) => r.json());

  // 3. BACKEND — vault card
  const { paymentInstrument } = await fetch('/api/payment-instruments', {
    method: 'POST',
    body: JSON.stringify({ tagadaToken, storeId, customerData: { email, firstName, lastName } }),
  }).then((r) => r.json());

  // 4. BACKEND — charge via payment flow
  const { payment } = await fetch('/api/payments', {
    method: 'POST',
    body: JSON.stringify({
      amount: 1900, currency: 'EUR', storeId,
      paymentInstrumentId: paymentInstrument.id,
      customerId: customer.id,
    }),
  }).then((r) => r.json());

  // 5. BACKEND — create subscription
  const { subscription } = await fetch('/api/subscriptions', {
    method: 'POST',
    body: JSON.stringify({
      customerId: customer.id, priceId, storeId, currency: 'EUR',
      defaultPaymentInstrumentId: paymentInstrument.id,
      paymentId: payment.id,
    }),
  }).then((r) => r.json());

  console.log('Subscribed:', subscription.id, subscription.status);
}
```

***

## Step 4 — Run the demo

```bash theme={null}
npm run dev
```

Open [http://localhost:5173](http://localhost:5173). The right panel shows your **payment flow** — primary processor and fallbacks.

***

## Test card

```
4242 4242 4242 4242   ·   12/30   ·   123
```

Works with the **sandbox** processor in the cascade flow. For live TPA charges, use a real card.

***

## Handle 3DS (usually nothing to do)

There are **two very different 3DS modes**, and most SaaS integrations only ever meet the first one:

### Option 1 — Platform / PSP-hosted 3DS (the default)

Modern PSPs (Adyen, Stripe, Checkout.com…) host the 3DS challenge themselves. You **never** call `threeds.createSession`. Just pass a `returnUrl` when charging; if the issuer requires SCA, the payment comes back with `requireAction: 'redirect'` and a `redirectUrl` pointing at the processor's own hosted 3DS page:

```ts theme={null}
const { payment } = await tagada.payments.process({
  amount: 1900, currency: 'EUR', storeId,
  paymentInstrumentId, customerId,
  initiatedBy: 'customer',
  paymentFlowId: process.env.TAGADA_PAYMENT_FLOW_ID,
  // REQUIRED for any flow that can challenge (EU cards on Adyen/Stripe):
  returnUrl: 'https://your-saas.com/billing/return',
});

if (payment.requireAction === 'redirect') {
  // Send the customer to the processor's hosted 3DS page.
  // After authentication they come back to your returnUrl —
  // resume with payments.continue():
  const { payment: resumed } = await tagada.payments.continue(payment.id);
}
```

Whether 3DS triggers is controlled by `threeDsEnabled: true` on your payment flow (or forced by the issuer). **How** it runs is the processor's business — you only handle `requireAction`.

### Option 2 — Standalone 3DS (the exception, rarely needed)

Some gateway-style processors (e.g. **NMI**) don't host 3DS but accept pre-authenticated values (CAVV, ECI…) on the charge. Only for those, Tagada runs the 3DS flow itself via `threeds.createSession` + `threedsSessionId`. If your flow routes to Adyen, Stripe, or a TagadaPay TPA, **skip this entirely**.

Full explanation: [Partners → Payments, Step 3](/developer-tools/partners/payments).

<Tip>
  **When in doubt:** don't pre-create anything. Charge with a `returnUrl`, then handle whatever `requireAction` the response carries.
</Tip>

***

## What happens on rebill?

TagadaPay automatically rebills the subscription each cycle. The charge routes through the same payment flow — cascade, weighted, fallback — with **no extra code**.

Manual rebill (support tool, retry script):

```ts theme={null}
await tagada.subscriptions.rebill('sub_xyz789');
```

Webhook events: `subscription/rebillSucceeded`, `subscription/rebillDeclined`, `subscription/pastDue`.

***

## Stripe mapping

| Stripe                                  | TagadaPay                                                 |
| --------------------------------------- | --------------------------------------------------------- |
| `stripe.customers.create`               | `tagada.customers.create`                                 |
| `stripe.paymentMethods.create` + attach | `core-js tokenize` → `paymentInstruments.createFromToken` |
| `stripe.paymentIntents.create`          | `tagada.payments.process`                                 |
| `stripe.subscriptions.create`           | `tagada.subscriptions.create`                             |
| `stripe.webhooks.constructEvent`        | `tagada.webhooks.constructEvent`                          |

Full migration guide: [Migrate from Stripe](/developer-tools/saas-billing/migrate-from-stripe).
