Authentication
Send your API key as a bearer token, understand what its scopes allow, and act on behalf of one of your sub-clients.
Send the key as a bearer token
Every request carries your API key in the Authorization header. That is the
whole credential. There is no session, no cookie, no signature and no second
header to derive.
curl "$TCB_API_BASE/me" \
-H "Authorization: Bearer $TCB_API_KEY"The header must be exactly Bearer, one space, then the key. A missing header,
a different scheme or extra whitespace returns 401.
Scopes
Your key carries a fixed list of scopes. A route checks for exactly one. A key
that does not hold it is refused with 403 and code: "INSUFFICIENT_SCOPE"
before the handler runs. Read your own list from GET /me, and ask TCB to
change it.
| Scope | Routes it unlocks |
|---|---|
position:read | GET /me, GET /sub-accounts, GET /positions, GET /positions/share-price-history |
account:read | GET /accounts, GET /accounts/{id} |
account:manage | POST /accounts, POST /accounts/{id}/iban, POST /accounts/{id}/close, POST /sub-accounts, POST /crypto-deposit-addresses |
transaction:read | GET /deposits, GET /deposits/{id}, GET /withdrawals, GET /withdrawals/{id}, GET /conversions/{id}, GET /payouts/{id}, GET /transfers, GET /transfers/{id}, GET /crypto-deposits, GET /crypto-deposits/{id} |
vault:deposit | POST /deposits |
vault:withdraw | POST /withdrawals |
trade:execute | POST /conversions |
payment:execute | POST /payouts |
transfer:execute | POST /transfers |
settlement:read | GET /settlements, GET /settlements/{id}, GET /settlement-requests, GET /settlement-requests/{id}, GET /settlement-holds, GET /settlement-holds/{id}, GET /settlement-returns, GET /settlement-returns/{id}, GET /network/profile, GET /network/resolve, GET /network/search, GET /network/connections, GET /netting/agreements, GET /netting/agreements/{id}, GET /netting/agreements/{id}/obligations, GET /netting/agreements/{id}/cuts, GET /netting/obligations/{id} |
settlement:manage | PUT /network/profile, POST /network/connections, DELETE /network/connections/{memberClientAccountId}, POST /settlement-requests, POST /settlement-requests/{id}/decline, POST /settlement-requests/{id}/cancel, POST /settlement-holds, POST /settlement-holds/{id}/cancel, POST /settlements/{id}/return-request, POST /settlement-returns/{id}/decline, POST /settlement-returns/{id}/cancel, POST /netting/agreements, POST /netting/agreements/{id}/accept, POST /netting/agreements/{id}/decline, POST /netting/agreements/{id}/end, POST /netting/obligations/{id}/withdraw |
settlement:execute | POST /settlements, POST /settlement-requests/{id}/approve, POST /settlement-holds/{id}/release, POST /settlement-returns/{id}/accept, POST /netting/agreements/{id}/obligations, POST /netting/agreements/{id}/cut |
statement:read | GET /statements, GET /statements/{id} |
sub-client:read | GET /sub-clients, GET /sub-clients/{id}, GET /sub-clients/{id}/identity, GET /sub-clients/{id}/attestations |
sub-client:manage | POST /sub-clients, PATCH /sub-clients/{id}, POST /sub-clients/{id}/attestations, POST /sub-clients/{id}/currencies, POST /sub-clients/{id}/close |
payee:read | GET /beneficiaries, GET /beneficiaries/{id} |
payee:manage | POST /beneficiaries, DELETE /beneficiaries/{id} |
webhook:manage | GET /webhooks, POST /webhooks, GET /webhooks/{id}, DELETE /webhooks/{id}, POST /webhooks/{id}/rotate-secret |
Ask for the narrowest set that does the job. A reporting process that never
moves money should not be holding vault:withdraw.
Acting on behalf of a sub-client
Once you hold sub-clients, you can execute a request as one of them by naming it in a header:
x-on-behalf-of: c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95The request then reads and writes that sub-client's own accounts and registry rows rather than yours. Your key's scopes still authorise the request. The header changes whose data the call touches, never what the key may do.
What happens when you send it
| If | Then |
|---|---|
| You omit the header | The request acts as you, against your own accounts and rows. |
| You name one of your own open sub-clients, on a route that accepts the header | The request executes as that sub-client. |
| You name a sub-client on a route that does not accept the header | 400 ON_BEHALF_OF_NOT_ALLOWED_HERE. The header is refused, never ignored. |
| You name an id that is not one of your sub-clients, does not exist at all, or is malformed | 404 ON_BEHALF_OF_NOT_FOUND. All three answer identically, so an id cannot be probed. |
| You name a closed sub-client | 409 ON_BEHALF_OF_SUB_CLIENT_CLOSED, on reads as well as writes. Its accounts are retired. |
| Your key is restricted to a subset of your sub-clients and you name one outside it | 403 ON_BEHALF_OF_NOT_ALLOWED_FOR_KEY. Only a restricted key ever sees this. |
| The sub-client exists and is open, but its verification does not yet permit the flow | 409 SUB_CLIENT_CAPABILITY_UNAVAILABLE. Fix that sub-client's verification, not your key. |
You send the same Idempotency-Key with a different x-on-behalf-of | 409 IDEMPOTENCY_KEY_REUSED. The header is part of the request fingerprint. |
Two further rules apply:
- The header cannot nest. A sub-client never has sub-clients of its own, so a request that is already acting as one cannot act as another.
- Your webhook subscriptions are unaffected. A sub-client holds no credential and no subscription of its own, so events about one are delivered to yours.
Which routes accept it
Accepts x-on-behalf-of | Refuses it |
|---|---|
GET /me | POST /deposits, GET /deposits, GET /deposits/{id} |
GET /accounts, GET /accounts/{id}, POST /accounts, POST /accounts/{id}/iban, POST /accounts/{id}/close | POST /withdrawals, GET /withdrawals, GET /withdrawals/{id} |
GET /sub-clients, GET /sub-clients/{id}, PATCH /sub-clients/{id}, GET /sub-clients/{id}/identity, GET /sub-clients/{id}/attestations, POST /sub-clients/{id}/attestations, POST /sub-clients/{id}/currencies, POST /sub-clients/{id}/close | POST /conversions, GET /conversions/{id} |
GET /transfers, GET /transfers/{id}, POST /transfers | POST /sub-accounts, GET /sub-accounts, GET /positions, GET /positions/share-price-history |
POST /payouts, GET /payouts/{id} | GET /statements, GET /statements/{id} |
Every settlement route: everything under /settlements, /settlement-requests, /settlement-holds, /settlement-returns, /network and /netting | POST /sub-clients, the /webhooks registry, the /beneficiaries registry |
The vault routes refuse the header: a position cannot be attributed to a sub-client through this contract. Use a vault sub-account to partition your position instead.
A worked request
Read one of your sub-client's accounts, as that sub-client:
curl "$TCB_API_BASE/accounts" \
-H "Authorization: Bearer $TCB_API_KEY" \
-H "x-on-behalf-of: c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95"Confirm who you are about to act as
GET /me accepts the header, so you can check which principal a call would run
as before you call anything that moves money:
{
"success": true,
"data": {
"partnerId": "9f1c2a54-7b3d-4e08-9a61-2c5f0b7d4e13",
"name": "Acme Payments",
"principalType": "SUB_CLIENT",
"environment": "test",
"scopes": ["position:read", "transaction:read", "vault:deposit", "vault:withdraw"],
"keyPrefix": "test_A7fQ2xM",
"clientAccountId": "3a7e5c91-0d24-4b6f-8e13-5c9a2f7b1d04",
"subClientId": "c4f81e2a-9b60-4d37-a512-8e0f3c6b7d95",
"subtreeRestricted": false
},
"timestamp": "2026-09-02T10:15:28.114Z"
}clientAccountId stays your own root whatever the header says.
subtreeRestricted tells you whether this key may act for every one of your
sub-clients or only some. A wrong id and a restricted key are therefore
distinguishable.
Authentication errors
| Status | Code | Meaning |
|---|---|---|
401 | — | Missing Authorization header, or a malformed one. |
401 | — | Unknown, revoked or expired key. All three read identically, so probing cannot tell them apart. |
401 | — | Your principal is not active. Contact TCB. |
403 | INSUFFICIENT_SCOPE | The key does not hold the scope this route requires. The body names the scope. |
403 | — | The key's principal type is not permitted on this endpoint. |
429 | MACHINE_RATE_LIMITED | You exceeded your per-principal rate limit. See Conventions. |
Errors
The full code catalogue and what to do about each one.
Sub-clients
Register end customers, get them verified, and hold money attributable to them.
Pilot disclosure
Environments and API keys
The pilot base URL, what is real and what is simulated in it, and how your API keys are issued, stored, rotated and revoked.
Banking as a service
Accounts and IBANs for you and for your end customers, money in, money moved between accounts, and money out to a registered destination.