Skip to main content

Server-to-server payments

Once you have a Processing Key (tp_sk_…) + a storeId, the entire payment lifecycle is yours. No checkout session, no funnel, no UI assumptions on our side.

The four steps

Steps 2 and 3 happen on your server. Step 1 happens in your customer’s browser. Step 4 closes the loop.

Step 1 — Tokenize the card (browser)

Use @tagadapay/core-js. The card data goes directly to BasisTheory’s vault — never to your server, never to ours:
PCI scope-out. Because the PAN never traverses your stack, your PCI obligations stay at SAQ-A. See @tagadapay/core-js for the full tokenization API.

Step 2 — Create a payment instrument (server)

Convert the single-use tagadaToken into a reusable paymentInstrument:
The same customer is reused if you provide a matching email — one customer can hold multiple instruments.
Address field names. The platform stores and validates addresses as address1, address2, postal, city, state, country (ISO 3166-1 alpha-2), plus firstName / lastName. The SDK still accepts the legacy line1 / line2 / postalCode aliases and maps them for you, but prefer the canonical names.
Addresses are optional — including for digital products. Neither billingAddress nor shippingAddress is required to charge: selling digital goods with no shipping address works out of the box. If you do provide an address, it is only forwarded to the acquirer when it carries a valid ISO 3166-1 alpha-2 country — an incomplete address (e.g. missing country) is safely omitted from the acquirer call instead of failing the payment. Providing a complete billingAddress is still recommended: it improves issuer approval rates (AVS) and 3DS frictionless outcomes.

Step 3 — 3DS (usually nothing to do)

Most integrations skip this step. Whether 3-D Secure runs, and how, is decided by the PSP/acquirer behind your TPA — and most of them handle it themselves. There are two ways it can happen:

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

Modern PSPs (Adyen, Stripe, …) own the 3DS challenge. You don’t call threeds.createSession. You just charge (Step 4) with a returnUrl; if the issuer requires SCA, payments.process comes back with requireAction: 'redirect' and a redirectUrl pointing at the processor’s own hosted 3DS page. You send the customer there, they authenticate, and they return to your returnUrl. Nothing to collect, nothing to pass through — the processor reconciles the authentication internally. This is the path the rest of this guide uses. If your TPA is on a hosted-3DS PSP, ignore the code below and go straight to Step 4.

Option 2 — Tagada standalone 3DS (the exception)

Some gateway-style processors (e.g. NMI) don’t host 3DS, but they do accept pre-authenticated 3DS values (CAVV, ECI, DS transaction id…) on the charge. For those — and only when the acquirer is compatible and takes those values as input — Tagada can run the 3DS flow for you, collect the values, and pass them raw to the acquirer. You opt in explicitly:
When does 3DS trigger? When the card flow has threeDsEnabled: true (set per-merchant via Payment Flows) or the issuer requests SCA. How it runs is up to the processor: hosted PSPs return requireAction: 'redirect' (Option 1); gateways that need raw values use the standalone session (Option 2). When in doubt, skip this step and just handle the requireAction that the charge returns.

Step 4 — Charge

Handling the response


Multi-TPA routing (cascade & weighted)

payments.process doesn’t hit a processor directly — it runs the payment flow attached to the storeId. A single merchant can own several TPAs (e.g. an Adyen sub-merchant and a Stripe Merchant-of-Record account) and route across them with no code change on your side: same process call, different storeId.
  • Cascade (failover) — the flow has a primary processor and an ordered fallback list (fallbackMode: true). If the primary declines (non-terminal), Tagada automatically retries the same charge on the next TPA, with a fresh payment method per attempt. Your code just sees the final captured.
  • Weighted — multiple processors with weights; each charge is routed to one TPA by weight.
A processing key restricted to one TPA resolves to that TPA’s merchant account, so it can charge any store of that merchant — including a store whose flow spans several of the merchant’s TPAs. You don’t need a separate key per processor.
You create and manage these flows yourself, over the API — see Payment flows (cascade & weighted). A runnable demo (picker for single / cascade / weighted, plus a “force the primary to decline” toggle) lives in partners-examples/02-merchant-checkout.

Auth + capture (split-flow)

Authorize now, capture later (hotels, car rentals, pre-orders, marketplaces):
You can also payments.cancel(auth.id) to release the authorization without capturing.

Refunds

Full refund:
Partial refund:
Refunds route through the same processor as the original charge — no decisions on your side.

Merchant-initiated transactions (MIT)

For subscription renewals, retry logic, or any charge initiated without the customer present:
The SDK reuses the original network transaction id transparently; you don’t have to track it yourself.

Listing and retrieving payments

A Processing Key sees only payments belonging to its TPA. Partner keys can list across all TPAs they own.

Webhooks

To learn about asynchronous events (3DS callbacks, settlement, disputes), set up webhooks at the TPA level:
See Webhooks & events for the full event reference and signature verification.

Errors you should handle

The SDK throws typed errors: