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