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.
End-to-end example
A minimal but complete partner integration. Three files: a signup script, a server route, and a browser page. Together they exercise the entire S2S flow.File 1 — Signup script (run once per merchant)
// scripts/onboard-merchant.ts
import Tagada from '@tagadapay/node-sdk';
const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!);
async function onboardMerchant(input: {
externalRef: string;
legalName: string;
country: string;
currency: string;
email: string;
}) {
// 1. Create the TPA + auto-store + acc_xxx (idempotent on externalRef)
const tpa = await partner.partners.accounts.create({
legalName: input.legalName,
country: input.country,
currency: input.currency,
externalRef: input.externalRef,
metadata: { onboardedBy: 'partner-script' },
});
console.log('TPA:', tpa.id);
console.log('Store:', tpa.storeId);
// 2. Mint a sub-key locked to that TPA (only on FIRST creation)
let subKeySecret: string | undefined;
if (tpa.created) {
const subKey = await partner.partners.apiKeys.create(tpa.id, {
label: `${input.externalRef} — server charges`,
});
subKeySecret = subKey.secret;
console.log('Sub-key:', subKey.prefix, '… (secret stored)');
}
// 3. Persist on your side
await yourDb.merchants.upsert({
where: { id: input.externalRef },
update: {
tagadaTpaId: tpa.id,
tagadaStoreId: tpa.storeId,
// only update secret if newly minted
...(subKeySecret && { tagadaSubKey: encrypt(subKeySecret) }),
},
create: {
id: input.externalRef,
tagadaTpaId: tpa.id,
tagadaStoreId: tpa.storeId,
tagadaSubKey: encrypt(subKeySecret!),
email: input.email,
},
});
return tpa;
}
await onboardMerchant({
externalRef: 'merchant_42',
legalName: 'Acme SAS',
country: 'FR',
currency: 'EUR',
email: 'ops@acme.com',
});
File 2 — Charging server (Express / Next.js / Hono / whatever)
// server/api/charge.ts
import Tagada from '@tagadapay/node-sdk';
export async function chargeHandler(req: Request) {
const body = await req.json();
const {
merchantId, // your merchant id
tagadaToken, // from the browser (card, apple_pay, or google_pay)
paymentMethod, // 'card' | 'apple_pay' | 'google_pay'
amount,
currency,
customerData,
} = body;
// 1. Resolve merchant + decrypt their sub-key
const merchant = await yourDb.merchants.findUnique({ where: { id: merchantId } });
const charging = new Tagada(decrypt(merchant.tagadaSubKey));
// 2. Create payment instrument
const { paymentInstrument, customer } = await charging.paymentInstruments.createFromToken({
tagadaToken,
storeId: merchant.tagadaStoreId,
customerData,
});
// 3. Charge
try {
const { payment } = await charging.payments.process({
paymentInstrumentId: paymentInstrument.id,
storeId: merchant.tagadaStoreId,
amount,
currency,
paymentMethod,
mode: 'purchase',
metadata: { yourOrderId: body.orderId },
});
if (payment.status === 'requires_action' && payment.requireAction === 'threeds') {
return Response.json({
status: 'threeds',
challenge: payment.requireActionData,
paymentId: payment.id,
});
}
return Response.json({
status: payment.status,
paymentId: payment.id,
customerId: customer.id,
});
} catch (err) {
if (err.code === 'card_declined') {
return Response.json({ status: 'declined', reason: err.failureReason }, { status: 402 });
}
throw err;
}
}
File 3 — Browser checkout page
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Acme — Checkout</title>
</head>
<body>
<h1>Pay €49.99</h1>
<!-- Express buttons (rendered conditionally based on paymentSetup) -->
<div id="express-buttons"></div>
<!-- Card form -->
<form id="card-form">
<input id="card-number" placeholder="Card number" />
<input id="card-expiry" placeholder="MM/YY" />
<input id="card-cvc" placeholder="CVC" />
<button type="submit">Pay with card</button>
</form>
<script type="module">
import {
Tokenizer,
isApplePayAvailable,
startApplePaySession,
isGooglePayAvailable,
startGooglePaySession,
} from 'https://esm.sh/@tagadapay/core-js@2';
// 1. Discover what this merchant has enabled
// (your server fetches this with the merchant's sub-key, then exposes
// the SAFE subset — never the publishable keys / processor IDs you don't trust on the client)
const setup = await fetch('/api/payment-setup?merchant=merchant_42').then(r => r.json());
const tokenizer = new Tokenizer({ environment: 'production' });
// 2. Render Apple Pay button if native is wired
if (setup.apple_pay?.enabled
&& setup.apple_pay.provider === 'apple_pay'
&& await isApplePayAvailable()) {
const btn = document.createElement('apple-pay-button');
btn.setAttribute('buttonstyle', 'black');
btn.setAttribute('type', 'pay');
btn.style.cursor = 'pointer';
btn.onclick = () => {
startApplePaySession(
{
basisTheoryApiKey: setup.apple_pay.metadata.basisTheoryApiKey,
countryCode: 'FR',
storeName: 'Acme',
},
{ currency: 'EUR', totalAmountMinor: 4999 },
{
onSuccess: async (tagadaToken, contacts) => {
await charge({
tagadaToken,
paymentMethod: 'apple_pay',
customerData: {
email: contacts.shippingContact?.emailAddress,
firstName: contacts.shippingContact?.givenName,
lastName: contacts.shippingContact?.familyName,
},
});
},
onError: (err) => alert(err.message),
onCancel: () => {},
},
);
};
document.getElementById('express-buttons').appendChild(btn);
}
// 3. Card form
document.getElementById('card-form').addEventListener('submit', async (e) => {
e.preventDefault();
const tagadaToken = await tokenizer.tokenizeCard({
cardNumber: document.getElementById('card-number').value,
expiryDate: document.getElementById('card-expiry').value,
cvc: document.getElementById('card-cvc').value,
});
await charge({
tagadaToken,
paymentMethod: 'card',
customerData: { email: prompt('Email?') },
});
});
async function charge({ tagadaToken, paymentMethod, customerData }) {
const res = await fetch('/api/charge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
merchantId: 'merchant_42',
tagadaToken,
paymentMethod,
amount: 4999,
currency: 'EUR',
customerData,
orderId: 'order_' + Date.now(),
}),
});
const result = await res.json();
if (result.status === 'captured') {
window.location = `/thank-you?payment=${result.paymentId}`;
} else if (result.status === 'threeds') {
// Embed result.challenge.url in an iframe and resume after callback
renderThreeDsChallenge(result.challenge);
} else if (result.status === 'declined') {
alert(`Declined: ${result.reason}`);
}
}
</script>
</body>
</html>
Same flow for Klarna (redirect APM)
The browser doesn’t do anything other than the click + final redirect:// Browser
async function payWithKlarna() {
const res = await fetch('/api/charge', {
method: 'POST',
body: JSON.stringify({
merchantId: 'merchant_42',
paymentMethod: 'klarna',
amount: 4999,
currency: 'EUR',
customerData: { email: 'jane@example.com' },
orderId: 'order_' + Date.now(),
}),
});
const { redirectUrl } = await res.json();
window.location.href = redirectUrl;
}
// Server — extend the existing handler with the redirect APM path
if (paymentMethod === 'klarna') {
// No tokenization, no payment instrument — just a customer + processor + redirect
const customer = await charging.customers.upsertByEmail({
storeId: merchant.tagadaStoreId,
email: customerData.email,
});
const { payment } = await charging.payments.process({
storeId: merchant.tagadaStoreId,
customerId: customer.id,
processorId: setup['klarna:stripe'].processorId,
paymentMethod: 'klarna',
amount,
currency,
mode: 'purchase',
returnUrl: `https://your-platform.com/checkout/return?order=${body.orderId}`,
});
return Response.json({
redirectUrl: payment.requireActionData.metadata.redirect.redirectUrl,
paymentId: payment.id,
});
}
Webhook handler (settle status server-side)
// server/api/webhooks/tagada.ts
import { verifyWebhookSignature } from '@tagadapay/node-sdk';
export async function tagadaWebhookHandler(req: Request) {
const signature = req.headers.get('tagada-signature');
const raw = await req.text();
const event = verifyWebhookSignature({
payload: raw,
signature,
secret: process.env.TAGADA_WEBHOOK_SECRET!,
});
switch (event.type) {
case 'payment.succeeded':
await yourDb.orders.update({
where: { externalRef: event.data.metadata.yourOrderId },
data: { status: 'paid', paymentId: event.data.id },
});
break;
case 'payment.failed':
await yourDb.orders.update({
where: { externalRef: event.data.metadata.yourOrderId },
data: { status: 'failed', failureReason: event.data.failureReason },
});
break;
case 'payment.refunded':
// ...
break;
}
return new Response('ok');
}
What you have now
A complete embedded-payments integration:| Capability | File |
|---|---|
| Provision merchants | scripts/onboard-merchant.ts |
| Per-merchant API key isolation | sub-key encrypted in your DB |
| Card payments (S2S) | server /api/charge + browser tokenization |
| Apple Pay (native) | browser button + server route |
| Klarna / iDEAL (redirect) | server /api/charge redirect path |
| 3DS challenge handling | requireAction === 'threeds' branch |
| Async settlement | webhook handler |
