Skip to main content

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:
CapabilityFile
Provision merchantsscripts/onboard-merchant.ts
Per-merchant API key isolationsub-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 handlingrequireAction === 'threeds' branch
Async settlementwebhook handler
No checkout sessions, no funnels, no Tagada-hosted UI. Your platform, your brand, our payments.