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.
| Field | Type | Required | Notes |
|---|---|---|---|
kind | "person" or "business" | yes | Decides which identity shape is expected. |
externalRef | string, up to 128 chars | yes | Your 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:. |
legalName | string, up to 255 chars | yes | The 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. |
currencies | string array | yes | ISO 4217, at least one. One MAIN account per entry, all receiving on the one IBAN of the sub-client's account. |
identity | object | no | The 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. |
attestation | object | no | See Attest to your due diligence. It is required before the sub-client can be activated; omitting it here leaves the sub-client REQUIREMENTS_DUE. |
metadata | object | no | Yours. Stored and returned untouched. |
idempotencyKey | string, up to 100 chars | no | Alternative 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.
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" }
}
}'{
"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.
| Level | Who performs the checks | What you send | What TCB does with it |
|---|---|---|---|
RELIANCE | You, under your own regulated programme | The activation baseline, your screening results, and an attestation that you carried out due diligence and screening | Records 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
externalRefyou 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
externalRefyou 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:
| Field | Type | Required | Notes |
|---|---|---|---|
registrationNumber | string | yes | Its registration number. |
legalForm | string | yes | E.g. "W.L.L.", "LLC", "Ltd". |
registeredAddress | object | yes | { 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. |
businessActivity | string | yes | What the business does. |
incorporation | object | yes | { 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. |
seniorManagement | array | yes | Free-form, stored and returned as sent. Must be non-empty. |
parties | array | conditionally | Every 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. |
noSignificantOwners | boolean | conditionally | Send 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[]:
| Field | Type | Notes |
|---|---|---|
role | string | One of director, shareholder, controller, ubo, trustee, settlor, beneficiary. |
firstName, lastName | string | For a natural-person party. |
legalEntityName | string | For a party that is itself a company — sent instead of firstName/lastName. |
nationality | string | ISO 3166-1 alpha-2. Not required for a corporate party. |
address | string | The party's country of residence, ISO 3166-1 alpha-2 — a country code, not a postal address. |
dateOfBirth | string | ISO 8601 date. Not required for a corporate party. |
nationalId | string | National ID for the party's jurisdiction. Not required for a corporate party. |
ownershipPercent | string | Decimal 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[].
{
"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.
| Status | When you see it | receive | sendInternal | sendExternal | holdSavings | holdCrypto / sendCrypto |
|---|---|---|---|---|---|---|
NOT_STARTED | Never on a sub-client you created — reserved | false | false | false | false | false |
REQUIREMENTS_DUE | Something in the activation baseline is outstanding, including right after a legalName or identity change, which re-runs TCB's own screening | false | false | false | false | false |
VERIFIED | Everything mandatory is present, attested and screened | true | true | true only where TCB granted it for this sub-client | false | true when you hold crypto, unless TCB set it otherwise for this sub-client |
RESTRICTED | TCB compliance set it; requirements.disabledReason says why | false | false | false | false | false |
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 whatPOST /payoutswithx-on-behalf-ofchecks: 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 alwaysfalse: 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 istruewhen you hold crypto (your KYC is approved and you have an active crypto account) andfalsewhen 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, andPOST /accountswithx-on-behalf-ofopens fiat accounts only, answering400 CURRENCY_NOT_SUPPORTEDfor a crypto asset. In the app, opening one without this capability is refused with409 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 asholdCrypto, 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.reason | What it means | What to do |
|---|---|---|
STATUS | The sub-client's verification.status is not VERIFIED yet. | Read requirements.currentlyDue[] and clear it. |
NOT_GRANTED | The 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. |
RESERVED | The 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.
| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | The wording agreed with TCB for your programme, sent verbatim. |
version | string, up to 64 chars | yes | The version string of that wording, e.g. "reliance-2026-09". |
cddPerformedAt | ISO 8601 | yes | When you performed the due diligence. |
cddStandard | "standard" or "enhanced" | yes | The depth of the diligence you performed. |
screeningResult | "clear", "match_reviewed_cleared" or "match" | yes | Send match_reviewed_cleared when a hit was raised and you cleared it. |
verificationMethod | string, up to 64 chars | yes | How you verified this customer's identity under your programme, e.g. "documentary", "electronic" or "biometric". |
verificationResult | "verified" or "not_verified" | yes | The outcome of that verification. A sub-client is activated only on verified. |
verifierRef | string, up to 255 chars | yes | Your 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" | yes | The politically-exposed-person result your screening returned. |
riskRating | "low", "medium" or "high" | yes | The customer risk rating your programme assigned. |
idempotencyKey | string, up to 100 chars | no | Alternative to the Idempotency-Key header. |
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"
}'{
"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
| Call | Scope | Returns |
|---|---|---|
GET /sub-clients | sub-client:read | A 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:read | One sub-client, the same object POST /sub-clients returned. |
GET /sub-clients/{id}/identity | sub-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:
| Param | Type | Notes |
|---|---|---|
q | string | Case-insensitive search across legal name, external ref and the identity record's email. |
country | string | ISO 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:
| Field | Type | Notes |
|---|---|---|
totalHeldUsd | decimal string | The 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. |
lastActivityAt | ISO 8601 or null | The latest transaction across the sub-client's own accounts. null if it has never had one. |
country | ISO 3166-1 alpha-2 or null | Derived 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.
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
| Field | Type | Notes |
|---|---|---|
legalName | string, up to 255 chars | A 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. |
identity | object | A partial record, merged into the stored one field by field. |
caps | object | Per-currency transfer ceilings for this sub-client, described below. |
metadata | object | Sent 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:
{ "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
| Status | Code | What to do |
|---|---|---|
| 400 | SUB_CLIENT_EXTERNAL_REF_RESERVED | Your externalRef starts with tcb:. Choose another. |
| 400 | CURRENCY_NOT_SUPPORTED | Ask TCB which currencies your programme covers. |
| 400 | IDENTITY_INVALID | A value cannot be stored as sent. details.fields[] lists the offending paths. |
| 400 | ATTESTATION_INCOMPLETE | A 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. |
| 400 | ATTESTATION_VERSION_UNKNOWN | The 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. |
| 400 | ATTESTATION_TEXT_MISMATCH | The text sent does not match the agreed wording for that version. details.version names it — send that version's text exactly as agreed. |
| 400 | SUB_CLIENT_TRANSFER_CAPS_INVALID | A caps value was not a decimal string, or a key was not a currency code. Nothing was applied — resend the whole object. |
| 400 | ON_BEHALF_OF_NOT_ALLOWED_HERE | You sent x-on-behalf-of to POST /sub-clients. Drop the header. |
| 403 | VERIFICATION_LEVEL_NOT_GRANTED | Your client account may not introduce sub-clients yet. Contact TCB. |
| 403 | INSUFFICIENT_SCOPE | The key lacks sub-client:read or sub-client:manage. |
| 404 | SUB_CLIENT_NOT_FOUND | No sub-client with that id under your root. An id from another tree answers the same way, so a guessed id tells you nothing. |
| 409 | SUB_CLIENT_EXTERNAL_REF_TAKEN | You already have a sub-client with that reference. |
| 409 | SUB_CLIENT_HAS_CRYPTO_BALANCE | Close refused. details.accountIds names the crypto accounts still holding an asset. It cannot be drained or sent; contact TCB. |
| 409 | SUB_CLIENT_HAS_BALANCE | Close refused. details.accountIds names the accounts still holding money. |
| 409 | SUB_CLIENT_HAS_PENDING_ACTIVITY | Close 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. |
| 409 | SUB_CLIENT_HAS_CRYPTO_DEPOSIT_IN_FLIGHT | Close 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. |
| 409 | SUB_CLIENT_CLOSED | The sub-client is closed and can no longer be modified. |
| 409 | SUB_CLIENT_CAPABILITY_UNAVAILABLE | The acting sub-client's capability for that flow is false. details.reason says which fix applies — see the reason table. |
| 409 | RELIANCE_PROGRAMME_RESTRICTED | TCB has restricted your reliance programme. No new sub-client may be activated until TCB lifts it; sub-clients already verified are unaffected. Contact TCB. |
| 409 | IDEMPOTENCY_KEY_REUSED | The 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.