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.

API keys & authentication

Partners use two kinds of keys, each with a different scope and a different blast radius if it leaks.

The two-key model

┌─────────────────────────────────────────────────────────────────┐
│  PARTNER KEY                                                    │
│  tp_sk_partner_live_xxx                                         │
│                                                                 │
│  Can: provision new TPAs, mint sub-keys, list TPAs, submit KYB  │
│  Used by: your provisioning pipeline / signup flow              │
│  Scope: ALL TPAs under your partnership                         │
└─────────────────────────────────────────────────────────────────┘

                       │ partners.apiKeys.create(tpaId)

┌─────────────────────────────────────────────────────────────────┐
│  SUB-KEY (per merchant)                                         │
│  tp_sk_live_xxx                                                 │
│                                                                 │
│  Can: tokenize, charge, refund, manage instruments — for ONE    │
│       specific TPA only                                         │
│  Used by: your charging code, in production                     │
│  Scope: a single TPA                                            │
└─────────────────────────────────────────────────────────────────┘
Partner keySub-key
Prefixtp_sk_partner_*tp_sk_*
ScopeAll your TPAsOne TPA
Provisioning powerspartners.*
Charging powers⚠️ requires explicit storeId per call✅ implicit (sub-key knows its TPA)
Where to useServer-only, signup pipelineServer-only, charging path
If leakedAll your merchants exposedOne merchant exposed
Never use the partner key in your charging path. Mint a sub-key for each merchant on creation, store it next to the merchant’s record on your side, and use only the sub-key thereafter. This minimizes blast radius.

Mint a sub-key

const subKey = await partner.partners.apiKeys.create('tpa_xxx', {
  label: 'merchant_42 — server charges',
});

// {
//   id: 'ak_xxx',
//   prefix: 'tp_sk_live_a1b2c3d4',     // safe to store/log/display
//   secret: 'tp_sk_live_a1b2c3d4...',  // RETURNED ONCE — store immediately
//   tagadapayAccountId: 'tpa_xxx',
//   label: 'merchant_42 — server charges',
//   createdAt: '2026-04-29T...',
// }
The secret field is only returned on creation. We hash it on storage and only keep the prefix for display. If you lose it, you must rotate.
your_db.merchants table:
  merchant_42:
    tagada_tpa_id           = 'tpa_xxx'
    tagada_store_id         = 'store_xxx'
    tagada_subkey_live_id   = 'ak_aaa'
    tagada_subkey_live      = (encrypted in your secret manager)
    tagada_subkey_test_id   = 'ak_bbb'
    tagada_subkey_test      = (encrypted in your secret manager)
Don’t share keys across merchants — the security boundary disappears if you do.

List sub-keys

const keys = await partner.partners.apiKeys.list('tpa_xxx');
// [{ id, prefix, label, createdAt, lastUsedAt, status }]
You see the prefix only. If you’ve lost the secret, your only path is to revoke and re-mint.

Revoke a sub-key

await partner.partners.apiKeys.revoke('ak_xxx');
// Subsequent calls with that secret get HTTP 401.
// Existing in-flight calls complete normally.
Revocation is immediate and irreversible. Mint a new key first, deploy it, then revoke the old one.

Rotation pattern

// 1. Mint a new key
const newKey = await partner.partners.apiKeys.create('tpa_xxx', { label: 'rotation 2026-04' });

// 2. Update your secret manager + redeploy your charging service
await yourSecretManager.set(`merchant_42.subkey_live`, newKey.secret);

// 3. After confirming no traffic on the old key (e.g. lastUsedAt > 24h ago), revoke
await partner.partners.apiKeys.revoke('ak_old_xxx');
A lastUsedAt timestamp is updated on every API call — useful to confirm safe revocation.

Authenticating requests

Both keys use the same Bearer header format:
Authorization: Bearer tp_sk_live_xxx
The SDK handles this for you:
const tagada = new Tagada(subKey.secret);
// All subsequent calls carry Authorization: Bearer tp_sk_live_xxx
If you call our API directly without the SDK:
curl -X POST https://api.tagadapay.com/api/public/v1/payments/process \
  -H "Authorization: Bearer tp_sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "storeId": "store_xxx", "paymentInstrumentId": "pi_xxx", ... }'

Scope enforcement

The server enforces scope on every request:
Key typeEndpoint reachedServer check
Partner key/v1/accounts (POST)✅ allowed
Partner key/v1/payments/process⚠️ allowed only if storeId belongs to a TPA owned by this partner
Sub-key/v1/accounts (POST)❌ 403 — sub-keys can’t provision new TPAs
Sub-key/v1/payments/process✅ allowed only if storeId matches the sub-key’s TPA
Sub-key for tpa_A/v1/payments/process with storeId of tpa_B❌ 403

Test mode

Test keys have prefix tp_sk_partner_test_* / tp_sk_test_*:
const partnerTest = new Tagada(process.env.TAGADA_PARTNER_TEST_KEY!);
const tpa = await partnerTest.partners.accounts.create({ legalName: 'Acme TEST', ... });
const subKeyTest = await partnerTest.partners.apiKeys.create(tpa.id, { label: 'test' });
Test mode:
  • Routes payments to test processors (Stripe test, etc.)
  • Returns mock 3DS challenges
  • Doesn’t move real money
  • Lives in a fully isolated namespace — you can’t mix test and live keys in the same client