Sub Accounts
Sub-accounts are merchant-owned child wallets with dedicated MPC keys. They let you isolate balances, scope delegated access, and route splits/offramp flows by sub-account.
Sub-accounts are created programmatically via API, SDK, or CLI. The dashboard intentionally provides a read-only watchtower view.
Authentication
All sub-account endpoints are protected merchant endpoints.
- Use
Authorization: Bearer zfi_test_... or Authorization: Bearer zfi_live_...
- Dashboard sessions also work for merchant UI usage.
Create Sub-Account
Immutable label for this sub-account. Must be unique per merchant.
Optional configured spend limit (USDC).
access_mode
string
default:"delegated"
delegated or merchant_managed.
Yield toggle metadata for this sub-account.
Example
curl -X POST https://api.zendfi.tech/api/v1/subaccounts \
-H "Authorization: Bearer zfi_test_your_key" \
-H "Content-Type: application/json" \
-d '{
"label": "user_paschal_001",
"spend_limit_usdc": 500,
"access_mode": "delegated",
"yield_enabled": false
}'
Response
{
"id": "sa_7b1w9j2k4m8p",
"merchant_id": "e9c1c4dc-7aa5-4ad5-90af-6e7035f4503d",
"wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"label": "user_paschal_001",
"status": "active",
"spend_limit_usdc": 500,
"access_mode": "delegated",
"session_key": null,
"yield_enabled": false,
"created_at": "2026-03-26T18:00:00Z"
}
List Sub-Accounts
Returns all sub-accounts under the authenticated merchant.
Get Sub-Account
GET /api/v1/subaccounts/{id}
id accepts either the UUID or external ID (for example sa_xxxxx).
Get Sub-Account Balance
GET /api/v1/subaccounts/{id}/balance
Response
{
"subaccount_id": "sa_7b1w9j2k4m8p",
"wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"usdc_balance": 125.42,
"sol_balance": 0.0192,
"accrued_yield": 0,
"yield_enabled": false,
"status": "active"
}
Mint Delegation Token
POST /api/v1/subaccounts/{id}/session-key
deposit_only, withdraw_only, spend_only, read_only, or full_access.
Optional spend cap enforced for this token.
Token lifetime in seconds.
Optional destination allowlist.
If true, token auto-revokes after first successful authorized use.
Response
{
"token_id": "8fe55f3b-7bd5-4768-a6df-6b28dbad1e61",
"subaccount_id": "sa_7b1w9j2k4m8p",
"scope": "withdraw_only",
"expires_at": "2026-03-26T18:15:00Z",
"spend_limit_usdc": 50,
"delegation_token": "satk_xxxxx"
}
delegation_token is returned once. Treat it like a secret and never log it.
Freeze Sub-Account
POST /api/v1/subaccounts/{id}/freeze
Optional body:
{ "reason": "fraud-review" }
Freezing revokes active delegation tokens and blocks sub-account activity.
Drain Sub-Account
POST /api/v1/subaccounts/{id}/drain
Drains funds from the sub-account wallet back to the merchant wallet.
Body
{
"token": "Usdc",
"amount": 25,
"mode": "live",
"passkey_signature": {
"credential_id": "...",
"authenticator_data": [1,2,3],
"signature": [4,5,6],
"client_data_json": [7,8,9]
}
}
Withdraw From Sub-Account
POST /api/v1/subaccounts/{id}/withdraw
Body
{
"to_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"amount": 10,
"token": "Usdc",
"mode": "live",
"delegation_token": "satk_xxxxx",
"passkey_signature": {
"credential_id": "...",
"authenticator_data": [1,2,3],
"signature": [4,5,6],
"client_data_json": [7,8,9]
}
}
Withdraw Sub-Account To Bank (One-Shot)
POST /api/v1/subaccounts/{id}/withdraw-bank
Creates a PAJ offramp order and automatically completes OTP verification using the same proxy-email automation model used by split bank withdrawals.
Body
{
"amount_usdc": 25,
"bank_id": "9PSB7A2A2LJZ3H6Q4G8XJ6A4",
"account_number": "0123456789",
"mode": "live",
"automation_token": "saatk_xxxxx",
"delegation_token": "satk_xxxxx",
"passkey_signature": {
"credential_id": "...",
"authenticator_data": [1,2,3],
"signature": [4,5,6],
"client_data_json": [7,8,9]
}
}
Response
{
"success": true,
"subaccount_id": "sa_7b1w9j2k4m8p",
"order_id": "17d8564c-6807-4f58-b8f0-c4338fb310b4",
"paj_order_id": "67f1d2f54d9f553abf9a2310",
"paj_deposit_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"bank_account_number": "0123456789",
"bank_account_name": "Paschal Okafor",
"amount_usdc": 25,
"fiat_amount": 38500,
"exchange_rate": 1540,
"fee": 0.25,
"status": "PAID",
"transaction_signature": "3k8...xyz"
}
automation_token and delegation_token are mutually exclusive.
Mint Sub-Account Automation Token
POST /api/v1/merchants/me/subaccounts/automation-tokens
Requires merchant dashboard session auth. Use this endpoint to mint bounded headless automation credentials for withdraw-bank.
Body
{
"sub_account_id": "sa_7b1w9j2k4m8p",
"ttl_seconds": 3600,
"max_uses": 25,
"total_limit_usdc": 500,
"per_tx_limit_usdc": 50,
"allowed_bank_ids": ["9PSB7A2A2LJZ3H6Q4G8XJ6A4"],
"allowed_account_numbers": ["0123456789"],
"mode": "live"
}
Response
{
"token_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"automation_token": "saatk_xxxxx",
"merchant_id": "e9c1c4dc-7aa5-4ad5-90af-6e7035f4503d",
"sub_account_id": "sa_7b1w9j2k4m8p",
"expires_at": "2026-03-26T20:00:00Z",
"max_uses": 25,
"total_limit_usdc": 500,
"per_tx_limit_usdc": 50,
"allowed_bank_ids": ["9PSB7A2A2LJZ3H6Q4G8XJ6A4"],
"allowed_account_numbers": ["0123456789"],
"mode": "live",
"created_at": "2026-03-26T19:00:00Z"
}
automation_token is returned once. Treat it as a secret and never log it.
Revoke Sub-Account Automation Token
POST /api/v1/merchants/me/subaccounts/automation-tokens/{token_id}/revoke
Immediately revokes the automation token.
Response
{
"success": true,
"token_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"status": "revoked"
}
This endpoint is API-key compatible and requires no manual OTP submission in your client. OTP handling is automated server-side via proxy email and IMAP monitor.
Close Sub-Account
DELETE /api/v1/subaccounts/{id}
Closes the sub-account, deactivates its wallet, and revokes associated delegation tokens.
Webhook Coverage
Sub-account flows emit:
- Withdrawal events:
WithdrawalInitiated, WithdrawalFailed, WithdrawalCompleted
- Lifecycle events:
SubAccountCreated, SubAccountDelegationTokenMinted, SubAccountFrozen, SubAccountClosed
See Webhooks for payload structure.