Pilot environment: testnet, simulated yield, mocked fiat rails — read the disclosure
Guides

Sub-clients

Onboard your end customers, attest to the due diligence you performed, and read what each one is allowed to do.

Concepts: Sub-clients in the object model

A sub-client is one of your end customers: a person or a business you onboard. Each sub-client holds its own account with TCB, in its own name. You collect the identity information and TCB accepts it. You instruct on the sub-client's behalf. TCB also runs its own screening on it. The sub-client holds no credential of its own.

A sub-client is not a vault sub-account. A vault sub-account partitions your own vault position. A sub-client is a registered end customer whose identity TCB records and verifies. There is no path from one to the other, and a vault sub-account that stands for one of your end customers must be linked to a registered sub-client.

Create a sub-client

POST /sub-clients · scope sub-client:manage · idempotency key required

Creates the sub-client and, in the same operation, reserves one MAIN account per currency you ask for. All of them receive on the sub-client's IBAN, which carries its own legal name as the payee name, and each carries local details for its currency where the currency has them (localRails). None of it is live yet: nothing accepts or moves money until the sub-client is VERIFIED.

FieldTypeRequiredNotes
kind"person" or "business"yesDecides which identity shape is expected.
externalRefstring, up to 128 charsyesYour identifier for this customer, unique across your own sub-clients. It is a separate namespace from the one your accounts and vault sub-accounts share. Letters, digits and . _ : -, starting with a letter or digit. Must not start with tcb:.
legalNamestring, up to 255 charsyesThe verified legal name. It is the payee name on every IBAN issued for this sub-client, and the name presented for payee-name checks where the scheme runs them, so it must match the identity record.
currenciesstring arrayyesISO 4217, at least one. One MAIN account per entry, all receiving on the one IBAN of the sub-client's account.
identityobjectnoThe identity record you collected. Send the activation baseline for this kind; anything still missing leaves the sub-client REQUIREMENTS_DUE rather than rejecting the call.
attestationobjectnoSee Attest to your due diligence. It is required before the sub-client can be activated; omitting it here leaves the sub-client REQUIREMENTS_DUE.
metadataobjectnoYours. Stored and returned untouched.
idempotencyKeystring, up to 100 charsnoAlternative to the Idempotency-Key header. Send one; sending both is allowed only if they match.

This is the one route on this page that refuses x-on-behalf-of. A sub-client never has sub-clients of its own, so the header answers 400 ON_BEHALF_OF_NOT_ALLOWED_HERE.

Request
curl -X POST https://api.threecommabank.com/api/partner/v1/sub-clients \
  -H "Authorization: Bearer $TCB_API_KEY" \
  -H "Idempotency-Key: acme-subclient-2026-09-02-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "person",
    "externalRef": "acme:cust-04231",
    "legalName": "Fatima Al-Khalifa",
    "currencies": ["EUR"],
    "identity": {
      "firstName": "Fatima",
      "lastName": "Al-Khalifa",
      "dateOfBirth": "1988-04-17",
      "nationality": "BH",
      "address": { "line1": "14 Road 2825", "city": "Manama", "country": "BH" }
    }
  }'
Response 201
{
  "success": true,
  "data": {
    "subClientId": "c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95",
    "kind": "person",
    "externalRef": "acme:cust-04231",
    "legalName": "Fatima Al-Khalifa",
    "status": "ACTIVE",
    "verification": {
      "level": "RELIANCE",
      "status": "REQUIREMENTS_DUE",
      "requirements": {
        "currentlyDue": ["identity.person.nationalId", "attestation", "screening"],
        "pastDue": [],
        "eventuallyDue": [],
        "disabledReason": null
      },
      "capabilities": {
        "receive": false,
        "sendInternal": false,
        "sendExternal": false,
        "holdSavings": false,
        "holdCrypto": false,
        "sendCrypto": false
      },
      "attestedAt": null,
      "verifiedAt": null
    },
    "accounts": [
      {
        "accountId": "7e2a0c53-4d81-49b6-8f37-1a5c9e0b2d74",
        "ownerType": "SUB_CLIENT",
        "ownerId": "c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95",
        "currency": "EUR",
        "role": "MAIN",
        "label": "EUR",
        "externalRef": null,
        "status": "ACTIVE",
        "balance": { "available": "0", "reserved": "0", "total": "0" },
        "iban": {
          "iban": "BH29TCBK00001234567890",
          "bic": "TCBKBHBMXXX",
          "holderName": "Fatima Al-Khalifa",
          "active": false
        },
        "capabilities": {
          "receive": false,
          "sendInternal": false,
          "sendExternal": false,
          "holdSavings": false,
          "holdCrypto": false,
          "sendCrypto": false
        },
        "createdAt": "2026-09-02T10:15:28.000Z",
        "disclosure": { "version": "2026-09-06", "summary": "…", "items": ["…"] }
      }
    ],
    "cryptoAccounts": [],
    "caps": {},
    "createdAt": "2026-09-02T10:15:28.000Z",
    "updatedAt": "2026-09-02T10:15:28.000Z",
    "disclosure": { "version": "2026-09-06", "summary": "…", "items": ["…"] }
  },
  "timestamp": "2026-09-02T10:15:28.412Z"
}

The sub-client exists, its accounts exist and its coordinate is allocated, but every capability is false and iban.active is false. Nothing can be sent to that IBAN and nothing can be paid from it until the sub-client is VERIFIED.

Every response on this page carries the same disclosure object printed at the top of this page. It is abbreviated in the examples. The account objects are the shape Accounts and transfers documents.

cryptoAccounts is the sub-client's crypto holdings, one entry per network: accountId, chain, status, the address the account receives on (null until it is ready; an account with no address cannot receive yet) and balances, one { asset, balance } per asset held, as decimal strings.

Understand the verification level

verification.level is RELIANCE, the only value the enum carries today. TCB decides which onboarding model your firm gets when it onboards you, and grants the level per client account. The decision turns on TCB's assessment of your firm — your regulator and permitted activities, your jurisdiction, your AML framework and the supervision you are under, your allocation controls, and TCB's ability to obtain information and records from you — together with a signed reliance agreement. Being regulated does not by itself qualify a firm for it.

LevelWho performs the checksWhat you sendWhat TCB does with it
RELIANCEYou, under your own regulated programmeThe activation baseline, your screening results, and an attestation that you carried out due diligence and screeningRecords what you send, runs its own screening at activation, derives the status and capabilities below, and samples and tests the files behind your attestations.

TCB may request supporting documents at any time, and retains sanctions and PEP governance, transaction monitoring, and periodic review of your firm. Your evidence stays with you, and you must be able to produce it to TCB without delay.

If your firm is not regulated, reliance is not available to it: your end customers are onboarded directly by TCB, on a TCB-operated verification level that is not available yet. level names the programme in force; further levels may be added later.

Give TCB the activation baseline

A sub-client is activated only once TCB holds the whole set below for it. Whatever is still outstanding is named in requirements.currentlyDue[].

For a person:

  • Legal name and any aliases.
  • Permanent residential address.
  • Date of birth.
  • Nationality.
  • Passport number, where applicable.
  • National ID details — CPR, Iqama or the equivalent for the jurisdiction.
  • Your own reference for this sub-client, which is the mapping between your customer and the sub-client TCB holds. That is the externalRef you send at creation.
  • The verification method, its result, the date you performed it, and your file reference for it.
  • The PEP result, the sanctions result and the risk rating your screening produced.

The last two lines ride the attestation; everything above them rides identity and externalRef.

For a business:

  • Legal name and any trading names.
  • Registration number.
  • Legal form.
  • Registered address.
  • Nature of business.
  • Date and country of incorporation.
  • Senior management.
  • Every owner or controller at or above 20 % — their own name, country of residence, date of birth, nationality and national ID — or an explicit statement that no one holds 20 % or more.
  • Your own reference for this sub-client, which is the mapping between your customer and the sub-client TCB holds. That is the externalRef you send at creation.
  • The verification method, its result, the date you performed it, and your file reference for it.
  • The PEP result, the sanctions result and the risk rating your screening produced.

The last two lines ride the attestation, like the person baseline above; everything above them rides identity and externalRef.

identity on a business:

FieldTypeRequiredNotes
registrationNumberstringyesIts registration number.
legalFormstringyesE.g. "W.L.L.", "LLC", "Ltd".
registeredAddressobjectyes{ line1, line2, city, region, postalCode, country } (ISO 3166-1 alpha-2 country). line1, city and country are mandatory; line2, region and postalCode are stored but never gate activation.
businessActivitystringyesWhat the business does.
incorporationobjectyes{ date, city, country } (ISO 8601 date, ISO 3166-1 alpha-2 country). date and country are both mandatory — a separate item from legalForm, not the same fact twice.
seniorManagementarrayyesFree-form, stored and returned as sent. Must be non-empty.
partiesarrayconditionallyEvery director, shareholder, controller, UBO, trustee or settlor/beneficiary you have on file — see the table below. Satisfied by at least one party at or above 20 % ownership or control, or by noSignificantOwners: true.
noSignificantOwnersbooleanconditionallySend true only when no natural person owns or controls the business at or above 20 %. There is no third way to say this: an empty parties[] reads as "not asked yet", never as "nobody qualifies".

Each entry in parties[]:

FieldTypeNotes
rolestringOne of director, shareholder, controller, ubo, trustee, settlor, beneficiary.
firstName, lastNamestringFor a natural-person party.
legalEntityNamestringFor a party that is itself a company — sent instead of firstName/lastName.
nationalitystringISO 3166-1 alpha-2. Not required for a corporate party.
addressstringThe party's country of residence, ISO 3166-1 alpha-2 — a country code, not a postal address.
dateOfBirthstringISO 8601 date. Not required for a corporate party.
nationalIdstringNational ID for the party's jurisdiction. Not required for a corporate party.
ownershipPercentstringDecimal string, e.g. "60".

A party at or above 20 % ownership is due on its own name, address, and — unless it is itself a company — dateOfBirth, nationality and nationalId, reported as identity.business.parties[<index>].<field> in requirements.currentlyDue[].

A business identity
{
  "kind": "business",
  "externalRef": "acme:cust-08812",
  "legalName": "Pearl Trading W.L.L.",
  "currencies": ["USD"],
  "identity": {
    "registrationNumber": "CR-114523",
    "legalForm": "W.L.L.",
    "registeredAddress": { "line1": "Bahrain Bay Tower", "city": "Manama", "country": "BH" },
    "businessActivity": "Payment services",
    "incorporation": { "date": "2019-02-01", "country": "BH" },
    "seniorManagement": [{ "firstName": "Layla", "lastName": "Mansour", "role": "CEO" }],
    "parties": [
      {
        "role": "ubo",
        "firstName": "Yousef",
        "lastName": "Al-Sayed",
        "nationality": "BH",
        "address": "BH",
        "dateOfBirth": "1978-06-30",
        "nationalId": "780630221",
        "ownershipPercent": "60"
      }
    ]
  }
}

TCB runs its own screening at activation, in addition to yours. While that screening is outstanding or under review, requirements.currentlyDue[] carries "screening". It is the one item on that list you cannot send: only TCB clears it, and the sub-client is not activated until it is gone.

The supporting documents — the passport image, the proof of address, the verification file — stay with you. TCB does not collect them at activation, and you must be able to produce them without delay.

Other information is not part of the activation set, and TCB asks for it when risk, monitoring or an investigation requires it: occupation, employer, source of funds, expected activity and contact details.

Nothing moves before VERIFIED

Creating a sub-client reserves its identifier and its MAIN accounts, and TCB allocates each IBAN internally. Until that sub-client is VERIFIED the coordinate is not active. It:

  • must not be given out — not to the sub-client, not to a payer, not to anyone;
  • is not entered into verification of payee;
  • accepts no funds: a payment addressed to it is not credited, and TCB parks it and returns or holds it under its own process;
  • holds no balance;
  • and cannot be paid from, or moved within your tree on the sub-client's behalf.

iban.active on the coordinate says which state it is in: false while the sub-client is anything other than VERIFIED, true once it is. Read it rather than inferring activation from the coordinate's existence.

Read what a sub-client may do

verification.status says where a sub-client stands. capabilities is what every money path checks. Both are derived, so they change the moment the underlying state changes.

StatusWhen you see itreceivesendInternalsendExternalholdSavingsholdCrypto / sendCrypto
NOT_STARTEDNever on a sub-client you created — reservedfalsefalsefalsefalsefalse
REQUIREMENTS_DUESomething in the activation baseline is outstanding, including right after a legalName or identity change, which re-runs TCB's own screeningfalsefalsefalsefalsefalse
VERIFIEDEverything mandatory is present, attested and screenedtruetruetrue only where TCB granted it for this sub-clientfalsetrue when you hold crypto, unless TCB set it otherwise for this sub-client
RESTRICTEDTCB compliance set it; requirements.disabledReason says whyfalsefalsefalsefalsefalse
  • receive — money may be credited to this owner.
  • sendInternal — this owner may be debited by a transfer inside your tree.
  • sendExternal — this owner may send money out of TCB. It is not granted by default, and it is what POST /payouts with x-on-behalf-of checks: the sub-client pays a destination from your registry out of its own account, over its own account details and under its own name.
  • holdSavings — reserved, and always false: the vault routes act for you, never for a sub-client.
  • holdCrypto — this owner may open crypto networks and receive on them; an address already opened keeps receiving. A sub-client follows you by default: it is true when you hold crypto (your KYC is approved and you have an active crypto account) and false when you do not. TCB can turn it on or off for one sub-client, and that decision then applies whatever you hold. Opening a further network for a sub-client is done in the app, acting as that sub-client: no route on this API opens one, and POST /accounts with x-on-behalf-of opens fiat accounts only, answering 400 CURRENCY_NOT_SUPPORTED for a crypto asset. In the app, opening one without this capability is refused with 409 SUB_CLIENT_CAPABILITY_UNAVAILABLE; asking for a network it already has returns that network.
  • sendCrypto — this owner may send crypto out. It defaults the same way as holdCrypto, and TCB can set it per sub-client in the same way. Sending crypto as a sub-client is done in the app; no route on this API sends crypto.

Only VERIFIED carries any capability at all. RESTRICTED is a compliance hold TCB places and only TCB lifts: nothing moves in or out while it stands, and the balance the sub-client already holds stays where it is — it is held, not returned.

requirements.currentlyDue[] carries dotted paths naming what is outstanding ("legalName", "identity.person.dateOfBirth", "attestation", "screening"). Everything you can act on comes first; "screening" is TCB's own check and is last. pastDue[] and eventuallyDue[] are empty today.

A money-moving request under x-on-behalf-of runs two independent checks and both must pass: your key's scope (403 INSUFFICIENT_SCOPE, fix the key) and the acting sub-client's capability for that flow (409 SUB_CLIENT_CAPABILITY_UNAVAILABLE, fix that sub-client's verification).

details.reason on that refusal tells you which of two different fixes applies, so read it rather than guessing from the message alone:

details.reasonWhat it meansWhat to do
STATUSThe sub-client's verification.status is not VERIFIED yet.Read requirements.currentlyDue[] and clear it.
NOT_GRANTEDThe sub-client is VERIFIED, but this specific capability is not on for it — sendExternal is not granted by default, and holdCrypto / sendCrypto are off when you do not hold crypto or TCB turned them off for this sub-client.Contact TCB to have it turned on for this sub-client.
RESERVEDThe capability is not grantable to any sub-client — holdSavings today.Not fixable per sub-client; it is a platform limit.

Read requirements.currentlyDue rather than hard-coding a field list

Everything TCB needs before it can activate a sub-client appears in requirements.currentlyDue[] while it is missing, as a dotted path into the request shape. Read that array on every response instead of keeping your own list: it is the one place the current set is published, and it changes without a new version of the API. Until it is empty and the status reads VERIFIED, the sub-client's coordinates are inactive and no money moves to it, from it or within your tree on its behalf.

Each dotted path names a field of the STORED identity record, not a request body to send verbatim: the person/business segment is dropped when you send it. identity.person.dateOfBirth is satisfied by PATCH { "identity": { "dateOfBirth": ... } }, not by { "identity": { "person": { "dateOfBirth": ... } } } — the latter is refused as 400 IDENTITY_INVALID, naming the unrecognised person key, rather than silently accepted and stored nowhere.

Attest to your due diligence

POST /sub-clients/{id}/attestations · scope sub-client:manage · idempotency key required

An attestation is your statement to TCB that you performed customer due diligence and screening on this customer under your own regulated programme. It is evidence that you did that work; it is not what activates the sub-client. Activation requires all four of: the mandatory identity data, this attestation, your screening results, and TCB's own screening.

Attestations are append-only: nothing is edited or deleted, and a newer one supersedes a stale one.

FieldTypeRequiredNotes
textstringyesThe wording agreed with TCB for your programme, sent verbatim.
versionstring, up to 64 charsyesThe version string of that wording, e.g. "reliance-2026-09".
cddPerformedAtISO 8601yesWhen you performed the due diligence.
cddStandard"standard" or "enhanced"yesThe depth of the diligence you performed.
screeningResult"clear", "match_reviewed_cleared" or "match"yesSend match_reviewed_cleared when a hit was raised and you cleared it.
verificationMethodstring, up to 64 charsyesHow you verified this customer's identity under your programme, e.g. "documentary", "electronic" or "biometric".
verificationResult"verified" or "not_verified"yesThe outcome of that verification. A sub-client is activated only on verified.
verifierRefstring, up to 255 charsyesYour own case, file or vendor reference for it. TCB stores it, does not resolve it, and uses it to ask you for the file.
pepStatus"none", "pep" or "pep_related"yesThe politically-exposed-person result your screening returned.
riskRating"low", "medium" or "high"yesThe customer risk rating your programme assigned.
idempotencyKeystring, up to 100 charsnoAlternative to the Idempotency-Key header.
Request
curl -X POST https://api.threecommabank.com/api/partner/v1/sub-clients/c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95/attestations \
  -H "Authorization: Bearer $TCB_API_KEY" \
  -H "Idempotency-Key: acme-attest-2026-09-02-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "We have performed customer due diligence and screening on this customer under our agreed reliance programme.",
    "version": "reliance-2026-09",
    "cddPerformedAt": "2026-08-28T09:00:00.000Z",
    "cddStandard": "standard",
    "verificationMethod": "documentary",
    "verificationResult": "verified",
    "screeningResult": "clear",
    "verifierRef": "acme-case-88214",
    "pepStatus": "none",
    "riskRating": "low"
  }'
Response 201
{
  "success": true,
  "data": {
    "attestationId": "b7f204e9-1c86-4a53-9e70-3d5f8b1c0a29",
    "subClientId": "c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95",
    "level": "RELIANCE",
    "text": "We have performed customer due diligence and screening on this customer under our agreed reliance programme.",
    "version": "reliance-2026-09",
    "cddPerformedAt": "2026-08-28T09:00:00.000Z",
    "cddStandard": "standard",
    "verificationMethod": "documentary",
    "verificationResult": "verified",
    "verifierRef": "acme-case-88214",
    "screeningResult": "clear",
    "pepStatus": "none",
    "riskRating": "low",
    "payloadSha256": "0f4c8b1d92a7e35604fd1c8a37b2905e6417da8c25f0b93e71c6482da05f3b9e",
    "receivedAt": "2026-08-28T09:00:04.000Z",
    "disclosure": { "version": "2026-09-06", "summary": "…", "items": ["…"] }
  },
  "timestamp": "2026-08-28T09:00:04.118Z"
}

payloadSha256 is a digest of the attestation exactly as received, so you can check what TCB recorded against what you sent.

An attestation missing a required field, or sent for a sub-client whose mandatory identity data is still outstanding, is refused with 400 ATTESTATION_INCOMPLETE and nothing is recorded. details.fields[] names what is missing, and requirements.currentlyDue[] on the sub-client says the same thing.

text and version must be the wording TCB agreed with you for your programme, sent verbatim, with that wording's own version string. A version that has been retired, or that was never agreed with you, is refused, and so is text that no longer matches the agreed wording for the version you sent — ask TCB for your programme's current version if you're unsure which to use.

GET /sub-clients/{id}/attestations (scope sub-client:read) returns { "items": [...] }, newest first.

Read your registry

CallScopeReturns
GET /sub-clientssub-client:readA page of sub-clients, newest first. Query cursor, limit (1–100, default 25), status (ACTIVE or CLOSED), externalRef, q, country and type. The body is { items, nextCursor }; a null nextCursor means this was the last page.
GET /sub-clients/{id}sub-client:readOne sub-client, the same object POST /sub-clients returned.
GET /sub-clients/{id}/identitysub-client:read{ subClientId, kind, person, business, updatedAt, disclosure } — what you supplied, as stored. TCB adds nothing to it.

GET /sub-clients query parameters, beyond paging and status:

ParamTypeNotes
qstringCase-insensitive search across legal name, external ref and the identity record's email.
countrystringISO 3166-1 alpha-2. Matches the derivation reported as the row's own country — see below.
type"person" or "business"Filters to one sub-client kind.

Each row on GET /sub-clients — and only that call — additionally carries:

FieldTypeNotes
totalHeldUsddecimal stringThe sum of every account the sub-client holds, FIAT and CRYPTO alike, priced in USD. Money held for a sub-client is money you ADMINISTER — it is never part of your own totals, and no aggregate ACROSS sub-clients is returned anywhere on this surface.
lastActivityAtISO 8601 or nullThe latest transaction across the sub-client's own accounts. null if it has never had one.
countryISO 3166-1 alpha-2 or nullDerived from the identity record: a person's permanent address, a business's incorporation country falling back to its registered address. null when neither is recorded.

GET /sub-clients/{id} and every write response omit all three — balances otherwise live on the accounts, not the registry row.

Request
curl -G https://api.threecommabank.com/api/partner/v1/sub-clients \
  -H "Authorization: Bearer $TCB_API_KEY" \
  --data-urlencode "q=khalifa" \
  --data-urlencode "country=BH" \
  --data-urlencode "type=person"

Update a sub-client

PATCH /sub-clients/{id} · scope sub-client:manage

FieldTypeNotes
legalNamestring, up to 255 charsA change on a VERIFIED sub-client moves verification.status back to REQUIREMENTS_DUE, with attestation in currentlyDue, and deactivates its IBANs until it is verified again. Nothing is re-issued: the same coordinates reactivate, carrying the corrected payee name, once it is verified again.
identityobjectA partial record, merged into the stored one field by field.
capsobjectPer-currency transfer ceilings for this sub-client, described below.
metadataobjectSent whole — it replaces the stored object.

Attestations are not patchable; append a new one instead.

A legalName or identity change re-runs TCB's own screening, whatever else the patch did or did not touch. "screening" can reappear in requirements.currentlyDue[] even on a sub-client that was already VERIFIED, and the sub-client stays inactive — every capability false, its IBANs deactivated — until TCB clears it again.

caps are the only ceilings that apply to a transfer inside your tree:

Request body
{ "caps": { "USD": { "perTransfer": "10000", "daily": "50000" } } }

Both ceilings are decimal strings, never JSON numbers, and both are inclusive. daily totals this sub-client's CROSS_OWNER debits over the UTC calendar day and resets at 00:00:00Z. A move between the sub-client's own accounts is never capped. The object is sent whole and replaces the stored one, so {} removes every ceiling and omitting the field leaves them untouched. A caps body that cannot be read is refused with 400 SUB_CLIENT_TRANSFER_CAPS_INVALID, and no part of it is applied. A breach at transfer time is 409 TRANSFER_CAP_EXCEEDED.

Add a currency

POST /sub-clients/{id}/currencies · scope sub-client:manage · idempotency key required

Send { "currency": "USD" }. Creates that currency's MAIN account and IBAN the same way POST /sub-clients does at creation, and returns the whole sub-client. A sub-client that already holds a MAIN in that currency answers 409.

Close a sub-client

POST /sub-clients/{id}/close · scope sub-client:manage · idempotency key required in the header, because this route takes no body

Closing is irreversible. Every account under the sub-client must hold a zero balance, and no movement against one of them may still be in flight. On success every IBAN and every crypto deposit address retires, and money sent to a retired IBAN or address is held by TCB and never credited. Returns the sub-client with status: "CLOSED".

A sub-client that holds any crypto is refused first, with SUB_CLIENT_HAS_CRYPTO_BALANCE: a closure drain moves fiat only, and a sub-client cannot send crypto yet. details.accountIds names the crypto accounts still holding an asset.

A crypto deposit to the sub-client that is still confirming also refuses the close, with 409 SUB_CLIENT_HAS_CRYPTO_DEPOSIT_IN_FLIGHT: closing retires the deposit address, and an arrival that confirmed afterwards would be held rather than credited. details.accountIds names the accounts and details.depositIds the deposits. Wait for them to confirm, then close.

The two fiat refusals are separate because they ask different things of you. SUB_CLIENT_HAS_BALANCE names accounts that still hold money, and you move it out. SUB_CLIENT_HAS_PENDING_ACTIVITY names accounts with a movement that has not reached a terminal state, and you wait: details.accountIds names them and details.pending[] says what is holding each. An account can read a zero balance and still be named — a payout debits it when it is admitted, and a return or a failure credits it back, into the account closing would have retired.

Handle the errors

StatusCodeWhat to do
400SUB_CLIENT_EXTERNAL_REF_RESERVEDYour externalRef starts with tcb:. Choose another.
400CURRENCY_NOT_SUPPORTEDAsk TCB which currencies your programme covers.
400IDENTITY_INVALIDA value cannot be stored as sent. details.fields[] lists the offending paths.
400ATTESTATION_INCOMPLETEA required attestation field is missing, or the sub-client's mandatory identity data is still outstanding. Nothing was recorded — send the whole attestation again once requirements.currentlyDue[] is satisfied.
400ATTESTATION_VERSION_UNKNOWNThe version sent is not one agreed for your programme, or it has been retired. details.version names it — ask TCB for your programme's current wording version.
400ATTESTATION_TEXT_MISMATCHThe text sent does not match the agreed wording for that version. details.version names it — send that version's text exactly as agreed.
400SUB_CLIENT_TRANSFER_CAPS_INVALIDA caps value was not a decimal string, or a key was not a currency code. Nothing was applied — resend the whole object.
400ON_BEHALF_OF_NOT_ALLOWED_HEREYou sent x-on-behalf-of to POST /sub-clients. Drop the header.
403VERIFICATION_LEVEL_NOT_GRANTEDYour client account may not introduce sub-clients yet. Contact TCB.
403INSUFFICIENT_SCOPEThe key lacks sub-client:read or sub-client:manage.
404SUB_CLIENT_NOT_FOUNDNo sub-client with that id under your root. An id from another tree answers the same way, so a guessed id tells you nothing.
409SUB_CLIENT_EXTERNAL_REF_TAKENYou already have a sub-client with that reference.
409SUB_CLIENT_HAS_CRYPTO_BALANCEClose refused. details.accountIds names the crypto accounts still holding an asset. It cannot be drained or sent; contact TCB.
409SUB_CLIENT_HAS_BALANCEClose refused. details.accountIds names the accounts still holding money.
409SUB_CLIENT_HAS_PENDING_ACTIVITYClose refused. A movement against one of the sub-client's accounts has not reached a terminal state. details.accountIds names them, details.pending[] says what is holding each. Wait for them to settle.
409SUB_CLIENT_HAS_CRYPTO_DEPOSIT_IN_FLIGHTClose refused. A crypto deposit to one of the sub-client's addresses is still confirming. details.accountIds names the accounts, details.depositIds the deposits. Wait for them to confirm.
409SUB_CLIENT_CLOSEDThe sub-client is closed and can no longer be modified.
409SUB_CLIENT_CAPABILITY_UNAVAILABLEThe acting sub-client's capability for that flow is false. details.reason says which fix applies — see the reason table.
409RELIANCE_PROGRAMME_RESTRICTEDTCB has restricted your reliance programme. No new sub-client may be activated until TCB lifts it; sub-clients already verified are unaffected. Contact TCB.
409IDEMPOTENCY_KEY_REUSEDThe same key with different parameters. Use a new key.

The full catalogue is in Errors.

Receive the events

Subscribe at POST /webhooks to sub_client.created, sub_client.verification.updated and sub_client.closed. A sub-client holds no credential and no subscription of its own, so an event about one is delivered to your subscription.

The machine-readable specification for these endpoints is openapi.partner.v1.json. Where it and this page disagree, the specification is authoritative.

Pilot disclosure

On this page