Skip to main content

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

POST /api/v1/subaccounts
label
string
required
Immutable label for this sub-account. Must be unique per merchant.
spend_limit_usdc
number
Optional configured spend limit (USDC).
access_mode
string
default:"delegated"
delegated or merchant_managed.
yield_enabled
boolean
default:"false"
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

GET /api/v1/subaccounts
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
scope
string
required
deposit_only, withdraw_only, spend_only, read_only, or full_access.
spend_limit_usdc
number
Optional spend cap enforced for this token.
expires_in_seconds
integer
default:"3600"
Token lifetime in seconds.
whitelist
array<string>
Optional destination allowlist.
single_use
boolean
default:"false"
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.