> ## 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.

# Processing provisioning (TPAs)

> Create TagadaPay Accounts (TPAs), handle KYB requirements and documents, and mint Processing Keys. A merchant can own several TPAs.

# Processing provisioning (TPAs)

To process payments through TagadaPay, a merchant needs a **TagadaPay Account (TPA)** — identifier prefix `tpa_xxx`. The TPA is the unit of KYB, payment routing, settlement, and authorization scope on the **Processing domain** (`/api/tagadapay/v1/partner/*`).

<Warning>
  **CRM-only partners** (`subscribed_products` has `crm` but not `payfac`) cannot call `tpas.create()` (`403 payfac_required`). Submit an entity application instead — jump to [Who assigns the acquirer → CRM-only](#who-assigns-the-acquirer--two-partner-situations), or the shared field reference in [Apply for TagadaPay Processing](/developer-tools/node-sdk/processing-applications).
</Warning>

<Note>
  **A merchant (`acc_xxx`) can own 0..N TPAs (`tpa_xxx`).** One TPA per settlement region is the common pattern (e.g. an EU TPA and a US TPA under the same merchant). Provision a [merchant](/developer-tools/partners/merchants) first, then attach TPAs to it via `accountId`.
</Note>

***

## What gets created

```
Merchant (acc_xxx)            ← CRM org — provisioned separately (or inferred)
  ├── TPA (tpa_xxx)           ← sub-merchant: KYB, keys, reporting, settlement
  └── Store (store_xxx)       ← checkout & payment routing (independent, via CRM)
```

When you create a TPA, a convenience store is auto-provisioned for the merchant. Store ↔ processor routing is handled by the CRM payment orchestration layer — the TPA and store are **independent entities**. Discover the store via `stores.list({ accountId })`. If you pass an `accountId`, the TPA attaches to that existing merchant; otherwise the surface infers/creates the owning merchant from the calling key.

<Note>
  **The merchant is never auto-invited to TagadaPay.** No welcome email and no portal login are created automatically. Your platform remains the primary interface unless you later request merchant portal access through your account manager.
</Note>

***

## The onboarding sequence

Driving a TPA from creation to `active` is always the same five steps, **in order**:

```
1. tpas.create()                    → TPA exists (pending)
2. requirements.update() × N        → business, representative AND banking.* values
3. documents.upload() × N           → KYB documents (bytes, multipart)
4. tpas.provision()                 → ★ pushes everything to the acquirer
   └── read the response: nextAction, requirements.dueCodes, rejectedDocuments
       fix what it points at, call provision() again — it converges.
5. merchant signs the agreement     → ★ mandatory in live mode
   └── sent automatically by email to representative.email once step 4
       completes; the TPA activates automatically on signature.
```

<Warning>
  **Step 4 is not optional.** Creating the TPA, filling requirements and uploading documents only stages the data on Tagada's side — **nothing is sent to the payment processor until you call `tpas.provision()`**. A TPA that never receives this call stays `pending_provisioning` forever. `provision()` is idempotent: re-calling it is always safe and only replays incomplete steps.
</Warning>

<Warning>
  **Step 5 is not optional either.** A live TPA cannot activate without a signed Merchant Service Agreement — this cannot be waived for platform merchants. The signing request is generated and emailed automatically (no API call needed), **branded to your partnership tier** (see [The merchant agreement](#the-merchant-agreement-mandatory-signature) below). The `provision()` response exposes the state in `contract`: relay `contract.signingUrl` to your merchant instead of relying on their inbox. Test-mode TPAs and MoR sub-merchants are exempt.
</Warning>

Two requirements deserve special attention because forgetting them is the most common cause of stuck onboardings:

* **`banking.*` (bank account)** — without it the merchant can never be paid out. Submit it with the other requirements, don't defer it.
* **Documents** — after `provision()` runs, call `documents.list()` and look for `status: 'rejected'`: the `rejectionReason` tells you exactly what to fix.

***

## Create a TPA

```ts theme={null}
const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!);

const tpa = await partner.partners.processing.tpas.create({
  legalName: 'Acme SAS',          // required — the KYB legal entity
  accountId: 'acc_xxx',          // owning merchant (acc_xxx)
  country: 'FR',                  // ISO 3166-1 alpha-2
  currency: 'EUR',                // ISO 4217 settlement currency
  externalRef: 'merchant_42_eu',  // YOUR id — used for idempotency
  markup: {                       // optional — YOUR price for THIS merchant
    cardMarkupBps: 100,           // +1.00% margin for you
    cardFixedCents: 10,           // +$0.10 margin for you
  },
  metadata: { region: 'eu' },
});

// →
// {
//   object: 'tpa',
//   id: 'tpa_xxx',
//   accountId: 'acc_xxx',
//   partnerId: 'par_xxx',
//   externalRef: 'merchant_42_eu',
//   status: 'pending_operator_assignment',
//   applicationStatus: 'created',
//   mode: 'live',
//   createdAt: '2026-04-29T…',
//   nextSteps: [
//     '1. Fill the requirements (accounts.updateRequirement) — including the banking.* codes…',
//     '2. Upload KYB documents (documents.upload — multipart bytes…)',
//     '3. Call tpas.provision(id) — NOTHING is sent to the acquirer until you do…',
//     '4. Inspect the provision() response: nextAction, requirements.dueCodes…',
//   ],
// }
//
// `country` / `currency` are provisioning inputs — they drive KYB and the
// auto-created store, but are not echoed back on the TPA object.
// Discover the store via: stores.list({ accountId: tpa.accountId })
```

<Note>
  **US businesses are supported.** Pass `country: 'US'` (and typically `currency: 'USD'`) — the KYB requirement set adapts to the entity's country (e.g. EIN + US bank details instead of an EU VAT number + IBAN). The rest of the flow (requirements → documents → `provision()`) is identical.
</Note>

### Who assigns the acquirer — two partner situations

What happens after processing intake depends on whether **you** have signed payfac / processing terms with TagadaPay:

<Tabs>
  <Tab title="Processing partner (signed acquirers)">
    You have an active buy-rate annex + PSP schedule for at least one acquirer (`acquirers.list()` returns them). Two options:

    * **Pin the acquirer yourself**: pass `acquirer: 'adyen'` at `create()`. The TPA skips the operator queue and goes straight to `pending_provisioning` — you own the full self-serve path to `active`.
    * **Let the router pick**: omit `acquirer` and, if your partner profile has a default routing config, the TPA is auto-assigned from your signed set.

    Your economics apply: Tagada's buy rate + your markup.

    For KYB UX you can still use [Embed onboarding (iframe)](/developer-tools/partners/embed-onboarding) — same draft/`entityId`/submit lifecycle as [tagadapay.io/apply](https://tagadapay.io/apply), partner-branded — or push data yourself via `requirements` / `documents` (API intake).
  </Tab>

  <Tab title="CRM-only partner (no processing contract)">
    You have **not** signed payfac terms (`subscribed_products` has `crm` but not `payfac`). You **cannot** call `tpas.create()` — the API returns `403 payfac_required`. Tagada assigns the acquirer / deal / contract after review.

    Use one of these intake routes (all land as **entity requests** in the ops inbox):

    **Route A — the merchant applies themselves.** Send them to [tagadapay.io/apply](https://tagadapay.io/apply).

    **Route C — Embed onboarding (iframe, partner-branded).** See [Embed onboarding (iframe)](/developer-tools/partners/embed-onboarding).

    **Route B — you apply on the merchant's behalf (SDK applications).** Same payload as merchant [processing-applications](/developer-tools/node-sdk/processing-applications):

    ```ts theme={null}
    const partner = new Tagada(process.env.TAGADA_PARTNER_KEY!);

    const merchant = await partner.partners.crm.merchants.create({ /* … */ });

    const application = await partner.partners.processing.applications.create({
      accountId: merchant.id,
      businessInfo: {
        businessName: 'Acme SAS',
        country: 'FR',
        // … recommended KYB fields — same as merchant docs
      },
      representative: {
        firstName: 'Jeanne',
        lastName: 'Dupont',
        email: 'jeanne@example.com',
      },
      bankAccount: { /* … */ },
      documents: [ /* … */ ],
    });
    // → { object: 'processing_application', id: 'ent_…', tpaId: null, status: 'submitted', … }

    // After Tagada promotes → TPA exists under merchant.id:
    const { key } = await partner.partners.processing.enroll({
      accountId: merchant.id,
      mode: 'live',
    });
    ```
  </Tab>
</Tabs>

### Capability matrix — CRM-only vs processing partner

The SDK surface is mostly shared. What changes for CRM-only is that **entity applications** replace shell `tpas.create`:

| Capability                                                | CRM-only partner                | Processing partner            |
| --------------------------------------------------------- | ------------------------------- | ----------------------------- |
| Merchants, CRM keys, stores, orders, customers            | ✅                               | ✅                             |
| `applications.create({ accountId, … })` / embed           | ✅ Entity Inbox                  | ✅ also available              |
| `tpas.create()`                                           | ❌ `403 payfac_required`         | ✅ self-serve                  |
| `requirements` / `documents` on an existing TPA           | After promote                   | ✅                             |
| `acquirers.list()`                                        | `[]`                            | Your signed acquirers         |
| Pin an acquirer at `create()`                             | n/a                             | ✅                             |
| `provision()` to `active`                                 | After operator promote + assign | Self-serve                    |
| Merchant pricing                                          | TagadaPay direct rates          | Your buy rate + markup        |
| Processing margin (`markup`)                              | ❌ no effect                     | ✅ your margin per charge      |
| Processing keys, charges, reads, webhooks (once `active`) | ✅                               | ✅                             |
| MoR sub-merchants (`subMerchants.create()`)               | ❌                               | ✅ if enabled for your partner |

### Idempotency

Calling `create()` again with the same `externalRef` returns the **same** TPA — no duplicates, safe to retry:

```ts theme={null}
const a = await partner.partners.processing.tpas.create({ legalName: 'Acme SAS', accountId: 'acc_xxx', externalRef: 'merchant_42_eu' });
const b = await partner.partners.processing.tpas.create({ legalName: 'Acme SAS', accountId: 'acc_xxx', externalRef: 'merchant_42_eu' });
console.log(a.id === b.id); // true
```

The `(partnerId, externalRef)` pair is permanent. Use a stable, unique ref — when a merchant has multiple TPAs, suffix your own id (`merchant_42_eu`, `merchant_42_us`).

***

## Per-merchant pricing (markup)

Each merchant's price is **Tagada's buy rate (your signed annex) + a markup — your margin**. By default the markup is your partner-level grid (see `acquirers.list()`), but you can price any merchant individually:

* Pass `markup` at `tpas.create()` (above), **or**
* Set / change it at any time — the new price applies to the very next charge:

```ts theme={null}
// Read the merchant's current pricing
const pricing = await partner.partners.processing.tpas.retrieveMarkup('tpa_xxx');
// { object: 'tpa_markup', tpaId, markup, source: 'tpa' | 'partner', sellRate: {...} }

// Set a per-merchant markup (replaces your partner-level grid for this TPA)
await partner.partners.processing.tpas.updateMarkup('tpa_xxx', {
  cardMarkupBps: 250,    // +2.50% for you
  cardFixedCents: 25,    // +$0.25 for you
});

// Revert to your partner-level grid
await partner.partners.processing.tpas.clearMarkup('tpa_xxx');
```

Rules to know:

* **Wholesale replacement, not a merge.** A per-TPA markup replaces your partner-level grid entirely for that merchant — fields you omit mean "no margin on that fee". `updateMarkup('tpa_xxx', {})` prices the merchant at Tagada's buy rate (zero margin for you).
* **Additive only.** Every field must be ≥ 0 — you can never price a merchant below Tagada's signed buy rate.
* **Instant.** The call refreshes the TPA's charge-time price snapshot before returning; `sellRate` in the response is what the merchant pays from the next charge on.
* Your margin is carved out per charge and lands on your partner balance account automatically — see [Payouts, balances & fees](/developer-tools/partners/payouts).

REST: `GET | PUT | DELETE https://api.tagadapay.io/api/tagadapay/v1/partner/tpas/:id/markup`.

***

## Retrieve / list

```ts theme={null}
const tpa = await partner.partners.processing.tpas.retrieve('tpa_xxx');
const byRef = await partner.partners.processing.tpas.retrieveByExternalRef('merchant_42_eu'); // null if none

const { data, hasMore } = await partner.partners.processing.tpas.list({
  limit: 50,
  accountId: 'acc_xxx',     // optional — only this merchant's TPAs
  externalRef: 'merchant_42_eu',
});

```

<Note>
  There is no general "update TPA fields" endpoint. Status transitions and KYB outcomes are **operator-driven** (your account manager moves them, or our auto-router does), so they are not settable from the SDK. Store ↔ processor routing is handled by the CRM payment orchestration layer.
</Note>

***

## Mint a Processing Key

A **Processing Key** (`tp_sk_…`) is scoped to this one TPA and used for charging and reporting.

```ts theme={null}
const key = await partner.partners.processing.tpas.keys.create('tpa_xxx');
// { object: 'processing_key', id: 'ak_xxx', tpaId: 'tpa_xxx',
//   prefix: 'tp_sk_live_…', secret: 'tp_sk_live_…' /* once */, label: 'Your Partner — merchant_42_eu' }

const keys = await partner.partners.processing.tpas.keys.list('tpa_xxx');
await partner.partners.processing.tpas.keys.revoke(key.id);
```

See [API keys & authentication](/developer-tools/partners/api-keys) for the full key model, and [Server-to-server payments](/developer-tools/partners/payments) to charge with it.

***

## KYB requirements

Every newly-created TPA is seeded with a default set of KYB requirements:

| Code                             | Category       | Description                                                                  |
| -------------------------------- | -------------- | ---------------------------------------------------------------------------- |
| `business.legal_name`            | business       | Exact legal name                                                             |
| `business.registration_number`   | business       | Company registration number (US: the 9-digit EIN — same as the tax ID)       |
| `business.country`               | business       | Country of registration                                                      |
| `business.tax_id`                | business       | Tax ID (EU: VAT number; US: the EIN — same value as the registration number) |
| `business.vat_number`            | business       | VAT number (where applicable)                                                |
| `business.website`               | business       | Public URL                                                                   |
| `representative.full_name`       | representative | Legal representative                                                         |
| `representative.dob`             | representative | Date of birth                                                                |
| `representative.address`         | representative | Residential address                                                          |
| `documents.identity_proof`       | documents      | Government ID                                                                |
| `documents.proof_of_address`     | documents      | \< 3 months old                                                              |
| `documents.company_registration` | documents      | Registration certificate                                                     |
| `banking.iban`                   | banking        | Settlement IBAN (must match company)                                         |

<Note>
  **US merchants:** the US has no separate company registration number, so the
  federal **EIN** (e.g. `98-1923816`) is both `business.tax_id` and
  `business.registration_number` — submit the same 9-digit value for both.
</Note>

You can customize this set per-partner via `applicationPreferences.defaultRequirements` — talk to your account manager.

### List + satisfy requirements

```ts theme={null}
const { data: requirements } = await partner.partners.processing.tpas.requirements.list('tpa_xxx');

// Submit a value for a non-document requirement. Pass the value directly —
// a primitive or a free-form object (e.g. { iban, bic } for banking.iban).
await partner.partners.processing.tpas.requirements.update(
  'tpa_xxx',
  'business.vat_number',
  'FR12345678901',
);
```

The requirement flips to `pending_verification` — never directly to `satisfied`. Tagada ops (or a downstream provider) verify the value before it becomes `satisfied`. Document requirements (`documents.*`) are satisfied by `documents.record()`, not this call.

### Upload a document (recommended)

The most reliable way to submit a document is to send the bytes directly — no storage URL to keep alive:

```ts theme={null}
import { readFile } from 'node:fs/promises';

const bytes = await readFile('./passport-jane-doe.pdf');
await partner.partners.processing.tpas.documents.upload('tpa_xxx', {
  file: new Blob([bytes], { type: 'application/pdf' }),
  kind: 'identity_proof',
  filename: 'passport-jane-doe.pdf',
  requirementCode: 'documents.identity_proof',
});
```

### Record a document by URL

Alternatively, `record()` a `storageUrl`. **The URL must be fetchable server-to-server at the moment you call `record()`** — we download and pin a copy immediately. Private buckets or already-expired presigned URLs are rejected with `422 document_unreachable` (a presigned URL that is valid *now* is fine, even if it expires later). The signature is **`record(tpaId, params)`**:

```ts theme={null}
await partner.partners.processing.tpas.documents.record('tpa_xxx', {
  requirementCode: 'documents.identity_proof',
  kind: 'identity_proof',
  filename: 'passport-jane-doe.pdf',
  // Must be an HTTPS URL we can download right now (e.g. a fresh presigned URL).
  storageUrl: 'https://your-bucket.s3.amazonaws.com/passport-jane-doe.pdf?X-Amz-...',
});

const { data: docs } = await partner.partners.processing.tpas.documents.list('tpa_xxx');
```

#### Accepted `kind` values

| `kind`                    | Use for                                                                                                                                                         |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identity_proof`          | Representative's photo ID (front). The concrete document type — passport, driver's license, ID card — is taken from the `representative.id_type` you submitted. |
| `identity_proof_back`     | Back side of a two-sided ID (driver's license, ID card). Paired automatically with the `identity_proof` front.                                                  |
| `company_registration`    | Registration certificate / articles of incorporation                                                                                                            |
| `proof_of_address`        | Company proof of address, \< 3 months old                                                                                                                       |
| `bank_statement`          | Bank statement for the settlement account                                                                                                                       |
| `constitutional_document` | Bylaws / operating agreement                                                                                                                                    |
| `proof_of_ownership`      | UBO / ownership structure document                                                                                                                              |

`kind` is **validated at the API boundary**: an unknown value (e.g. your own internal enum like `INCORPORATION_CERTIFICATE`) is rejected immediately with `400 invalid_document_kind` listing the accepted values — it can never sit in the pipeline and fail silently later. Obvious variants are normalized for you (case/separators, `certificate_of_incorporation` → `company_registration`, `driving_license` → `drivers_license`, …), but send the canonical value. Specific identity kinds (`passport`, `drivers_license`, `national_id`) are also accepted when you know the exact document type.

#### Validation & idempotency

| Rule                   | Error                               | Meaning                                                                                           |
| ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
| Size 1 KB – 10 MB      | `file_too_small` / `file_too_large` | A file under 1 KB is a placeholder or an error body, never a real document.                       |
| Known `kind`           | `invalid_document_kind`             | Unknown kinds are rejected upfront with the accepted list.                                        |
| Fetchable `storageUrl` | `document_unreachable` (422)        | We download and pin the bytes at `record()` time.                                                 |
| Same bytes + same kind | *(no error)*                        | Idempotent: the existing document is returned (HTTP 200), no duplicate is created. Safe to retry. |

Recording a document linked to a requirement automatically flips that requirement to `pending_verification`. We don't auto-approve — manual review (or processor-driven verification) flips it to `satisfied`.

<Warning>
  Check `documents.list()` after provisioning runs: a document whose delivery to the payment processor failed shows `status: 'rejected'` with a `rejectionReason` explaining what to fix (unreachable URL, missing front side, unsupported kind…). A document stuck without an accepted status blocks activation.
</Warning>

***

## Run provisioning

Once requirements and documents are in, call `provision()` — and treat it as a **converge loop**, not a one-shot:

```ts theme={null}
const result = await partner.partners.processing.tpas.provision('tpa_xxx');

if (!result.completed) {
  console.log(result.nextAction);
  // e.g. "Fill the outstanding requirements (banking.iban), then call provision again."

  console.log(result.requirements.dueCodes);   // exact codes still due
  console.log(result.rejectedDocuments);       // [{ kind, rejectionReason, … }]
}
```

| Field                   | What it tells you                                                                  |
| ----------------------- | ---------------------------------------------------------------------------------- |
| `completed`             | `true` when every pipeline step ran without error.                                 |
| `nextAction`            | One line: what to do next (`null` when completed).                                 |
| `requirements.dueCodes` | The exact requirement codes still `currently_due` / `past_due`.                    |
| `rejectedDocuments[]`   | Documents that failed delivery to the acquirer, each with a `rejectionReason`.     |
| `contract`              | Merchant Service Agreement state: `{ required, status, signingUrl, signerEmail }`. |
| `steps[]`               | Per-step outcome of the activation pipeline.                                       |

Fix what the response points at, then call `provision()` again — completed steps are never redone.

### The merchant agreement (mandatory signature)

Signing the Merchant Service Agreement is **a hard activation condition** for every live TPA. You never send it yourself: once provisioning completes, Tagada emails the signing request to the merchant representative automatically.

How the contract is branded depends on your partnership tier:

| Tier                                        | Who contracts with the merchant                                                      | What the PDF looks like                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **CRM partner** (software white-label)      | TagadaPay LLC                                                                        | Your logo and name in the header, a "Scope & Partner Relationship" clause stating the agreement covers **payment processing only** and is offered exclusively through your platform, plus an **indicative software-fee schedule** (your CRM fees, dated, non-contractual) so merchants understand the two invoices.                                                                        |
| **Payfac partner** (processing white-label) | **You** — your legal entity is the contracting party and counter-signs the agreement | Fully branded to you. TagadaPay is disclosed as platform operator (with the registered acquirer, as card-scheme rules require) and your responsibility is backed by your Master Services Agreement with TagadaPay. If a contract signatory is configured on your partner profile, your representative counter-signs electronically; the account activates once **both** signatures are in. |

```ts theme={null}
const result = await partner.partners.processing.tpas.provision('tpa_xxx');

if (result.contract?.status === 'pending_signature') {
  // Surface this link in YOUR dashboard / onboarding flow —
  // don't wait for the merchant to find the email.
  console.log(result.contract.signingUrl);
  console.log(result.contract.signerEmail); // who received the request
}
// status: 'not_required' | 'not_sent' | 'pending_signature' | 'signed'
```

The TPA flips to `active` automatically the moment the required signatures are in — no further API call needed. Test-mode TPAs and MoR sub-merchants never require a signature (`required: false`).

### Common integration mistakes

| Mistake                                 | Consequence                                                              | Fix                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| Never calling `provision()`             | TPA stays `pending_provisioning` forever — nothing reaches the acquirer. | Call it after pushing requirements + documents (step 4 of the sequence).           |
| Skipping `banking.*` requirements       | KYB may pass but payouts can never be configured.                        | Submit the bank account with the other requirements.                               |
| `record()` with a private / expired URL | `422 document_unreachable` (previously: silent late failure).            | Use `documents.upload()` with the raw bytes, or mint a fresh presigned URL.        |
| Sending internal enum names as `kind`   | `400 invalid_document_kind` (previously: silent late failure).           | Use the kinds from the table above.                                                |
| Never checking `documents.list()`       | Rejected documents block activation invisibly.                           | After each `provision()`, inspect `rejectedDocuments` / `documents.list()`.        |
| Ignoring the contract step              | Provisioning completes but the TPA never activates.                      | Relay `contract.signingUrl` to the merchant; activation is automatic on signature. |

***

## Merchant portal access policy

By default, sub-merchants have **no TagadaPay portal access** — they use your platform, not ours. If you ever want a specific merchant to be able to log in directly to TagadaPay, three policy modes exist:

| Mode                     | Behavior                                                                                                                   |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `'never'`                | Merchant **never** gets a TagadaPay login. Operators see a warning if they try to invite.                                  |
| `'on_request'` (default) | Stub account exists, no auto-invite. Invitations are dispatched out-of-band by your account manager.                       |
| `'auto_invite'`          | Send the merchant a magic link the moment the TPA is created. Use only if the merchant expects a TagadaPay-branded portal. |

Set the partner-wide default via `applicationPreferences.merchantPortalAccess` (talk to your account manager), or override per-TPA at creation time.

<Note>
  For the typical embedded-payments partner (your merchants don't know TagadaPay exists), `'never'` is the right setting and prevents any accidental email leak.
</Note>

***

## What you cannot do as a partner

| Action                                  | Why                                                                                        |
| --------------------------------------- | ------------------------------------------------------------------------------------------ |
| Read another partner's TPAs             | Partner key scope: you only see TPAs where `partnerId = your_par_xxx`                      |
| Move a TPA across merchants             | Would cross merchant entities — refused server-side                                        |
| Delete a TPA                            | TPAs are status-archived (`offboarded`), never deleted — required for audit/payout history |
| Edit KYB-locked fields after activation | `country`, `currency` become immutable once the TPA reaches `active`                       |
