Skip to main content

SaaS Billing Quick Start

Time: ~15 minutes | Difficulty: Intermediate This walkthrough matches the tested 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
  • 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):
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:
ResourceExample IDPurpose
Payment flowflow_35f3975175feCascade: sandbox → tagadapay-router (TPA)
Productproduct_…SaaS Pro Plan
Priceprice_…€19.00/month recurring
Verify everything works:
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.
server/index.ts
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);
});
Never expose TAGADA_API_KEY to the browser. The frontend only sends tagadaToken to your backend.

Step 3 — Frontend checkout (core-js)

The browser tokenizes the card and calls your API routes.
web/src/App.tsx
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

npm run dev
Open 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:
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.
When in doubt: don’t pre-create anything. Charge with a returnUrl, then handle whatever requireAction the response carries.

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):
await tagada.subscriptions.rebill('sub_xyz789');
Webhook events: subscription/rebillSucceeded, subscription/rebillDeclined, subscription/pastDue.

Stripe mapping

StripeTagadaPay
stripe.customers.createtagada.customers.create
stripe.paymentMethods.create + attachcore-js tokenizepaymentInstruments.createFromToken
stripe.paymentIntents.createtagada.payments.process
stripe.subscriptions.createtagada.subscriptions.create
stripe.webhooks.constructEventtagada.webhooks.constructEvent
Full migration guide: Migrate from Stripe.