@tagadapay/node-sdk
v3.28.0
Published
Official Tagada Node.js SDK — PSP/gateway-agnostic payment orchestration
Readme
@tagadapay/node-sdk
Official server-side Node.js SDK for TagadaPay — the PSP-agnostic payment orchestration platform.
One client (new Tagada(apiKey)), three jobs:
- CRM — manage stores, products, orders, customers, subscriptions, promotions, funnels, and checkout pages from your server.
- Payments & Processing — process and route payments across PSPs, and onboard onto TagadaPay as your payment processor.
- Partners — provision and manage accounts on behalf of your sub-merchants.
📚 Full documentation: https://docs.tagada.io/developer-tools/node-sdk/quick-start
Install
npm install @tagadapay/node-sdkRequires Node.js 18+.
Quick start
import Tagada from '@tagadapay/node-sdk';
const tagada = new Tagada('your-api-key');
// List your stores
const stores = await tagada.stores.list();
// Process a payment — TagadaPay routes it across your connected processors
const { payment } = await tagada.payments.process({
amount: 4999,
currency: 'USD',
storeId: 'store_xxx',
paymentInstrumentId: 'pi_xxx',
// The shopper behind this call, as seen by YOUR server (Express behind a
// trusted proxy shown). Processors weigh the shopper IP heavily; Tagada cannot
// read it from your request, and never sends your server's address instead.
customer: { ipAddress: req.ip, userAgent: req.get('user-agent') },
});payments.process() is server-to-server. It charges a vaulted instrument — first payment when you already have the order, or a later charge (MIT — merchant-initiated, customer not present). It does not pay a Headless checkout session. When TagadaPay manages the cart, the browser uses @tagadapay/headless-sdk processPayment(). When you already have the order, tokenize with @tagadapay/core-js then call this. See Choose how you accept payments.
Need an API key? Run the interactive initializer (writes .env):
npx -p @tagadapay/node-sdk@latest tagada-init [email protected]…or create one from Dashboard → Settings → Access Tokens. Full guide: Get an API key.
Authentication — keys & scopes
The SDK is one client; what you can call depends on the key you pass.
| Key | Format | Domain | Scope |
| --- | --- | --- | --- |
| CRM Key | sk_crm_ + 48 hex characters (legacy UUID accepted) | CRM | one merchant account (acc_xxx) |
| Processing Key | tp_sk_live_… / tp_sk_test_… | Processing | one TagadaPay account (tpa_xxx) — charging |
| Partner Key | tp_sk_live_… / tp_sk_test_… (partner-scoped) | both | provisioning on behalf of your merchants |
const tagada = new Tagada(process.env.TAGADA_API_KEY!);1 · CRM
Manage everything around the sale: catalog, customers, orders, recurring billing, marketing, checkout pages, and infrastructure. Authenticated with a CRM Key, served on the CRM API.
| Resource | What it does |
| --- | --- |
| tagada.stores | Create and manage stores |
| tagada.products | Products with variants and multi-currency pricing |
| tagada.orders | Query orders |
| tagada.customers | Look up and manage customers |
| tagada.subscriptions | Recurring billing — create, cancel, rebill, migrate processor |
| tagada.promotions / tagada.promotionCodes | Discounts and discount codes |
| tagada.offers / tagada.checkoutOffers | Upsells, downsells, order bumps |
| tagada.blockRules | Fraud / block rules |
| tagada.funnels | Multi-step funnels (checkout → upsell → thank-you), and your own pages — see External steps below |
| tagada.shopify | Manage the Shopify checkout script — bind the storefront to a funnel, remove it, audit the theme |
| tagada.plugins | Deploy SPAs to the edge CDN, A/B split testing, custom domains |
| tagada.domains | Add, verify, configure, and remove custom domains |
| tagada.emailTemplates | Transactional email templates |
| tagada.shippingRates | Delivery options surfaced at checkout |
| tagada.taxExemptions | Exempt specific customers from tax |
| tagada.webhooks | Register webhook endpoints (+ signature verification) |
| tagada.events | Event log, statistics, and audit trail |
const store = await tagada.stores.create({
name: 'My Store',
baseCurrency: 'USD',
presentmentCurrencies: ['USD', 'EUR'],
chargeCurrencies: ['USD'],
});
const product = await tagada.products.create({
storeId: store.id,
name: 'Premium Plan',
active: true,
variants: [{ name: 'Monthly', sku: 'premium-monthly', price: 2999, default: true, active: true }],
});→ Full walkthrough (store → product → funnel → live checkout link): Node SDK Quick Start.
External steps — put your own pages on the canvas
Your quiz, your onboarding, your paywall: pages you host yourself. Report what happens on them and they appear on the merchant's canvas next to the checkout and the upsells.
// Once, at deploy time: what your steps are called and where they lead.
await tagada.funnels.declare(store.id, {
manifestVersion: 1,
key: 'main',
name: 'Main funnel',
domains: ['shop.example.com'],
steps: [
{ id: 'quiz', type: 'page', name: 'Skin quiz', path: '/quiz',
events: [{ name: 'passed' }, { name: 'abandoned' }] },
],
});
// Then, in the server route handler that serves it.
await tagada.funnels.reportStep({
storeId: store.id, stepId: 'quiz', event: 'view',
});
// And when the visitor leaves by a named exit.
await tagada.funnels.reportStep({ storeId: store.id, stepId: 'quiz', exit: 'passed' });Omit url from reportStep for pages declared by a manifest: URL-bearing reports can also
auto-create legacy external blocks. These server reports use the legacy tracking endpoint;
use the Headless SDK's track.step() and event helpers for SDK step analytics.
Keep the same manifest key and step id on every deploy. declare upserts the manifest,
using the same API as tagada funnel push.
reportStep sends traffic to POST /funnel-tracking/step, which takes an eventType and has
no exit field: event is sent as eventType, and an exit is sent as eventType: 'custom' with
the exit name in customEventName. Passing exit therefore always sends a custom event.
Migrating from declareSteps
funnels.declareSteps(storeId, steps) is deprecated. Replace it with
funnels.declare(storeId, manifest) as above, or declare the same manifest with tagada funnel push.
Use one stable funnel key; keep each step's id and name, add type: 'page', and split its
absolute url into a bare host in domains and a route in path. Turn named exits into
events: [{ name: 'passed' }]; use next: ['offer'] for destination step ids, not exit names.
Named events do not automatically route visitors. For browser tracking of declared SDK steps,
use the Headless SDK's tagada.track.step() and event helpers from the
Coded Funnels guide.
Remove the old declaration call when switching. The two APIs write separate registries
(funnels_v2 external nodes versus sdk_funnels/sdk_steps), so calling both can show two blocks
for one page. Existing legacy blocks are not removed or reconciled by this change; review and
remove obsolete blocks on the canvas. declareSteps remains callable with its existing arguments,
requests, and results, and now logs a warning once per process.
Coded funnels — declare a whole funnel you code
When every page of the funnel is yours (built with @tagadapay/headless-sdk), declare it once in
tagada.funnel.ts and push it. Each step becomes a card on the canvas; the browser reports the
steps with tagada.track.step() and the SDK fires the pixels bound to them.
// tagada.funnel.ts
import { defineFunnel } from '@tagadapay/node-sdk';
export default defineFunnel({
key: 'main',
name: 'Main funnel',
domains: ['shop.example.com'],
steps: [
{ id: 'landing', type: 'page', name: 'Landing', path: '/' },
{ id: 'checkout', type: 'checkout', name: 'Checkout', path: '/checkout' },
],
});npx tagada funnel push # declare it (TAGADA_API_KEY + TAGADA_STORE_ID, or --api-key= --store=)
npx tagada funnel check # SDK health: steps never seen, blocked pixels, outdated SDK…
npx tagada funnel csp # the CSP origins a Tagada checkout needsFrom code: funnels.declare(), funnels.uploadStepImage(), funnels.health(), validateManifest(),
CSP_ORIGINS / withTagadaCsp(). @tagadapay/node-sdk/edge exports the manifest and CSP helpers
and the async webhook verifiers with no node: import, for Cloudflare Workers, Vercel Edge and Deno.
Guide: Coded Funnels.
2 · Payments & Processing
Two things live here: processing payments, and onboarding onto TagadaPay as your processor so you can go live.
Process & route payments
| Resource | What it does |
| --- | --- |
| tagada.payments | Process, refund, and void payments |
| tagada.checkout | Server-side checkout sessions and payments |
| tagada.paymentInstruments | Vaulted (stored) payment methods — incl. createFromCard() for PCI-compliant merchants |
| tagada.paymentFlows | PSP routing — cascade, weighted, failover |
| tagada.processors | Connect and manage PSP credentials (incl. Stripe Connect OAuth) |
| tagada.threeds | 3D Secure — run it with us, or report your own MPI result (createFromValues()) |
| tagada.paymentSetup | Runtime payment configuration for a store |
Pay a checkout session
checkout.pay() requires the checkout's customer session token; the merchant API key alone is not sufficient.
Extract the token query parameter from the redirect URL returned by checkout.createSession() and pass it
in opts.headers['x-cms-token']. The separate checkoutToken value identifies the checkout and is not the
customer session token. Use a valid, unexpired customer session token for the checkout being paid.
Pass the shopper's IP and user agent in customer: your server's own address is never taken for the shopper's.
const session = await tagada.checkout.createSession({
storeId: 'store_xxx',
items: [{ variantId: 'variant_xxx', quantity: 1 }],
});
const cmsToken = new URL(session.redirectUrl).searchParams.get('token');
if (!cmsToken) throw new Error('Missing customer session token');
// Use the checkout session ID and tokenized instrument for this checkout.
const result = await tagada.checkout.pay(
{
checkoutSessionId,
paymentInstrumentId,
customer: { ipAddress: req.ip, userAgent: req.get('user-agent') },
},
{ headers: { 'x-cms-token': cmsToken } },
);TagadaPay is PSP-agnostic: connect multiple processors and let payment flows route intelligently with automatic fallbacks.
const flow = await tagada.paymentFlows.create({
data: {
name: 'US Primary',
strategy: 'cascade', // 'simple' = single PSP, 'cascade' = fallback chain
pickProcessorStrategy: 'weighted',
processorConfigs: [
{ processorId: 'proc_stripe', weight: 60 },
{ processorId: 'proc_adyen', weight: 40 },
],
fallbackProcessorConfigs: [{ processorId: 'proc_nmi', orderIndex: 0 }],
},
});Onboard onto TagadaPay (become live)
To accept payments through TagadaPay, a merchant submits a KYB application;
once approved, our team provisions a TagadaPay account (tpa_xxx) you manage
and charge on.
Best practice — minting a key is a one-time setup step, not app code. Applying and minting your processing key is a bootstrap action (think seeding script, or a click in the dashboard). Do it once, store the secret in your secrets manager, then have your application read it from the environment. Never call
enroll()/tpas.keys.create()in a request path.
// ── setup.ts — run ONCE (one-off script / CLI / CI bootstrap) ──────────────
// Submit an application with your CRM key. `recommendations` lists KYB gaps;
// `documentWarnings` lists document/declared-data mismatches to fix for direct
// acquirer acceptance.
const app = await tagada.processing.applications.create({
businessInfo: { businessName: 'Acme SAS', country: 'FR' },
representative: { firstName: 'Jane', lastName: 'Smith', email: '[email protected]' },
});
if (app.documentWarnings?.length) {
for (const w of app.documentWarnings) console.warn(`[${w.code}] ${w.message}`);
}
// Optional: pre-check a single document right after uploading it, BEFORE submit.
const audit = await tagada.processing.applications.auditDocument({
kind: 'passport',
fileUrl: uploadedUrl,
representative: { firstName: 'Jane', lastName: 'Smith', nationality: 'FR' },
});
// audit.issues: [] means it looks consistent with the declared data.
// Once approved, enroll ONCE to mint your processing key, then store the secret.
// enroll() is the programmatic equivalent of activating processing in the
// TagadaPay dashboard and copying your key. It is NOT idempotent — a second
// call mints another key, so run this once and persist the result.
const { key } = await tagada.processing.enroll({ mode: 'live' });
console.log('Store in your secret manager as TAGADA_PROCESSING_KEY:', key.secret);// ── app.ts — your application, every request ───────────────────────────────
// Read the stored key from the environment. Never mint keys here.
const tp = new Tagada(process.env.TAGADA_PROCESSING_KEY!); // tp_sk_…
const { data: tpas } = await tp.processing.tpas.list();
await tp.payments.process({ /* … */ });tagada.processing.tpas.* exposes keys, requirements, and documents to
fill KYB and mint per-account charging keys. → Apply for TagadaPay Processing.
3 · Partners namespace
If you run a platform that onboards and charges for many merchants, the
partners.* namespace provisions on their behalf. Requires a Partner Key.
Same rule applies: provisioning and key minting belong in your onboarding flow, not your checkout path. Each minted secret is returned once — persist it in your vault keyed by merchant, then read it back when charging.
const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!); // partner-scoped tp_sk_…
// 1. Create a CRM merchant (idempotent on externalRef). Pass `email` to
// invite your client to the dashboard in the same call: they receive
// an email branded as YOU (your name, logo, colors — not TagadaPay's),
// set their own password, and land in the CRM as admin of their org.
const merchant = await partner.partners.crm.merchants.create({
legalName: 'Acme SAS',
externalRef: 'merchant_42',
email: '[email protected]', // optional — omit for API-only merchants
});
// merchant.portalInvite → { status: 'invited', emailMode: 'branded', … }
// Created the merchant without an email? Invite them later (idempotent):
await partner.partners.crm.merchants.invite(merchant.id, { email: '[email protected]' });
// Mint a CRM key to read/manage the merchant's data from your backend.
const crmKey = await partner.partners.crm.merchants.keys.create(merchant.id);
// 2. Provision a TagadaPay account (TPA) + mint a processing key.
const tpa = await partner.partners.processing.tpas.create({
legalName: 'Acme SAS', // required
accountId: merchant.id, // attach to the merchant above
country: 'FR',
currency: 'EUR',
externalRef: 'merchant_42_eu', // idempotency
});
const procKey = await partner.partners.processing.tpas.keys.create(tpa.id);
// 3. Submit KYB requirements & documents as they arrive.
await partner.partners.processing.tpas.requirements.update(tpa.id, 'legal_name', 'Acme SAS');partners.crm.merchants.*— provision merchants, invite them to the dashboard (create({ email })/invite()), mint CRM keyspartners.processing.tpas.*— provision accounts, mint processing keys, KYB requirements/documentspartners.processing.acquirers.list()— the acquirers you've signed (pin one viatpas.create({ acquirer }))
A complete runnable boilerplate (CLI provisioning + Express checkout) lives at github.com/TagadaPay/partners-examples. → Partners guide.
Versions and the Payments API
This SDK talks to the Commerce API (https://api.tagada.io/api/public/v1). Its payment calls are
V2 payments: payments.process(), refund(), void(), checkout.pay(), threeds.*.
| Payments interface | Supported |
| --- | --- |
| V2 payments (Commerce API) | Yes |
| Payments API, V3 (https://api.tagada.io/v1, in preview) | Not yet. A tagada.payments namespace for V3 is coming soon. |
- Stores switched to the Payments API keep working. TagadaPay switches stores one by one. On a
switched store,
payments.process()(includingpaymentFlowId),refund()andvoid()are answered through a compatibility layer. Do not change your code. - Retries: pass an
idempotencyKeyso a retried call never charges twice:
const { payment } = await tagada.payments.process(
{
amount: 1999,
currency: 'USD',
storeId: 'store_xxx',
paymentInstrumentId: 'pi_xxx',
customer: { ipAddress: req.ip, userAgent: req.get('user-agent') },
},
{ idempotencyKey: `order_${orderId}` },
);X-Tagada-SDK: since 3.27.1, every request sendsX-Tagada-SDK: node-sdk/<version>.apiVersionsets anX-Tagada-Api-Versionheader. The Commerce API does not read it today.
See SDK versions and V2 or V3.
Error handling
import Tagada, {
TagadaAPIError,
TagadaNotFoundError,
TagadaValidationError,
TagadaAuthenticationError,
TagadaRateLimitError,
} from '@tagadapay/node-sdk';
try {
await tagada.payments.retrieve('pay_nonexistent');
} catch (err) {
if (err instanceof TagadaNotFoundError) {
// 404
} else if (err instanceof TagadaValidationError) {
console.log(err.errors); // field-level errors
} else if (err instanceof TagadaRateLimitError) {
console.log(`Retry after ${err.retryAfter}s`);
}
}Every API error also carries the machine-readable code and human message
returned by the API — branch on err.code, log err.message:
try {
await partner.partners.processing.tpas.create({ accountId, legalName, acquirer: 'adyen' });
} catch (err) {
if (err instanceof TagadaAPIError) {
err.code; // e.g. 'acquirer_not_available', 'no_acquirer_assigned',
// 'mor_sub_merchant_not_enabled', 'partner_scope_required'
err.message; // e.g. 'Acquirer "adyen" is not available to partner par_… Signed acquirers: (none).'
err.statusCode; // 422
}
}Retries on 429, 5xx, and network errors are automatic (tune via maxRetries).
Pass an idempotencyKey to safely retry mutations. The key must be a UUID: create it once per operation
(for a payment, once per payment), store it, and reuse it on every retry. A key that is not a UUID is ignored without an error.
A 5xx response releases the key, so a retry after a 5xx can charge again.
import { randomUUID } from 'node:crypto';
const idempotencyKey = randomUUID(); // store it with the order, reuse it on retry
await tagada.payments.process(params, { idempotencyKey });Configuration
const tagada = new Tagada({
apiKey: 'your-api-key',
// All optional — sensible production defaults are used when omitted.
baseUrl: 'https://api.tagada.io/api/public/v1',
timeout: 30_000,
maxRetries: 2,
apiVersion: '2025-01-01',
});TypeScript
Fully typed. Import any type you need:
import type { Payment, Customer, Subscription, Tpa, TagadaList } from '@tagadapay/node-sdk';Full guide
License
© Tagada Pay. All rights reserved.
Experiments, flags and server events
// Node SDK (>= 3.25.0)
const tagada = new Tagada({ apiKey: process.env.TAGADA_API_KEY!, analytics: { storeId: '{storeId}' } });
const variant = await tagada.experiments.getVariant('{key}', userId);
const on = await tagada.flags.isEnabled('{flagKey}', userId);
await tagada.flush(); // before a serverless function returnsRender your default when a variant is null. Config loads on first use, stays fresh for 30 seconds,
and refreshes in the background. Reading a running external experiment queues one exposure per
store, key, distinct id and phase. getBootstrap sends no exposures; pass its result to the browser
or headless SDK. Distinct ids must contain 1–128 printable ASCII characters.
await tagada.identify(userId, { customerId, email });
await tagada.events.send([{ name: 'quiz_done', distinctId: userId, properties: { score: 3 },
idempotencyKey: `quiz:${quizId}`, ip: buyerIp, userAgent: buyerUserAgent }]);
await tagada.shutdown();Forward the buyer's IP and user agent from the storefront. The server hashes IP, email and phone
before storing analytics rows. Reserved event names such as order_completed are rejected.
events.send reports accepted/rejected counts and throws typed API errors; flush and shutdown
settle without rejecting. Use analytics.disabled: true to disable every analytics call.
A missing store id rejects with TagadaValidationError; opts.storeId overrides analytics.storeId.
No analytics writes are sent until the server returns an enabled config. This version is not
published by the analytics lane; distribution follows the team's normal release.
