> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zendfi.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Sub Accounts

> Manage merchant-controlled sub-accounts, delegation tokens, and scoped withdrawals.

# 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.

<Note>
  Sub-accounts are created programmatically via API, SDK, or CLI. The dashboard intentionally provides a read-only watchtower view.
</Note>

## 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

```http theme={null}
POST /api/v1/subaccounts
```

<ParamField body="label" type="string" required>
  Immutable label for this sub-account. Must be unique per merchant.
</ParamField>

<ParamField body="spend_limit_usdc" type="number">
  Optional configured spend limit (USDC).
</ParamField>

<ParamField body="access_mode" type="string" default="delegated">
  `delegated` or `merchant_managed`.
</ParamField>

<ParamField body="yield_enabled" type="boolean" default="false">
  Yield toggle metadata for this sub-account.
</ParamField>

### Example

```bash theme={null}
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

```json theme={null}
{
  "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

```http theme={null}
GET /api/v1/subaccounts
```

Returns all sub-accounts under the authenticated merchant.

***

## Get Sub-Account

```http theme={null}
GET /api/v1/subaccounts/{id}
```

`id` accepts either the UUID or external ID (for example `sa_xxxxx`).

***

## Get Sub-Account Balance

```http theme={null}
GET /api/v1/subaccounts/{id}/balance
```

### Response

```json theme={null}
{
  "subaccount_id": "sa_7b1w9j2k4m8p",
  "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "usdc_balance": 125.42,
  "sol_balance": 0.0192,
  "accrued_yield": 0,
  "yield_enabled": false,
  "status": "active"
}
```

***

## Get Merchant Sub-Account TTL Policy

```http theme={null}
GET /api/v1/subaccounts/ttl-policy
```

Returns effective TTL ceilings, merchant overrides, and platform hard caps.

### Response

```json theme={null}
{
  "merchant_id": "e9c1c4dc-7aa5-4ad5-90af-6e7035f4503d",
  "policy": {
    "effective": {
      "signing_grant_max_ttl_seconds": 1209600,
      "automation_token_max_ttl_seconds": 1209600,
      "child_delegation_max_ttl_seconds": 259200
    },
    "merchant_overrides": {
      "signing_grant_max_ttl_seconds": 1209600,
      "automation_token_max_ttl_seconds": 1209600,
      "child_delegation_max_ttl_seconds": 259200
    },
    "platform_hard_caps": {
      "signing_grant_max_ttl_seconds": 7776000,
      "automation_token_max_ttl_seconds": 7776000,
      "child_delegation_max_ttl_seconds": 7776000
    }
  }
}
```

***

## Update Merchant Sub-Account TTL Policy

```http theme={null}
POST /api/v1/subaccounts/ttl-policy
```

Updates merchant-specific TTL ceilings. Each provided value must be within the platform hard-cap range.

### Body

```json theme={null}
{
  "signing_grant_max_ttl_seconds": 1209600,
  "automation_token_max_ttl_seconds": 1209600,
  "child_delegation_max_ttl_seconds": 259200
}
```

Response shape matches `GET /api/v1/subaccounts/ttl-policy`.

***

## Mint Delegation Token

```http theme={null}
POST /api/v1/subaccounts/{id}/session-key
```

<ParamField body="scope" type="string" required>
  `deposit_only`, `withdraw_only`, `spend_only`, `read_only`, or `full_access`.
</ParamField>

<ParamField body="spend_limit_usdc" type="number">
  Optional spend cap enforced for this token.
</ParamField>

<ParamField body="expires_in_seconds" type="integer" default="3600">
  Token lifetime in seconds.
</ParamField>

<ParamField body="whitelist" type="array<string>">
  Optional destination allowlist.
</ParamField>

<ParamField body="single_use" type="boolean" default="false">
  If true, token auto-revokes after first successful authorized use.
</ParamField>

<ParamField body="policy_version_id" type="string">
  Optional policy version UUID attached to this token.
</ParamField>

<ParamField body="agent_label" type="string">
  Optional actor label for audit attribution.
</ParamField>

<ParamField body="agent_public_key" type="string">
  Optional actor public key for attribution.
</ParamField>

<ParamField body="agent_metadata" type="object">
  Optional structured actor metadata.
</ParamField>

### Response

```json theme={null}
{
  "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"
}
```

<Warning>
  `delegation_token` is returned once. Treat it like a secret and never log it.
</Warning>

***

## Mint Child Delegation Token

```http theme={null}
POST /api/v1/subaccounts/{id}/session-key/child
```

Mints an attenuated child token from an existing parent delegation token.

`expires_in_seconds` must be within the merchant effective child-delegation TTL ceiling and cannot exceed parent token expiry.

### Body

```json theme={null}
{
  "parent_delegation_token": "satk_parent_xxxxx",
  "scope": "withdraw_only",
  "spend_limit_usdc": 25,
  "expires_in_seconds": 900,
  "whitelist": ["7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"],
  "single_use": true,
  "policy_version_id": "a6e9db6f-e264-4f9d-a4c7-7f94b18f0f34",
  "agent_label": "risk-bot-v2",
  "agent_public_key": "ed25519:...",
  "agent_metadata": {
    "workflow": "vendor-payout"
  }
}
```

### Response

```json theme={null}
{
  "token_id": "d5a8012b-95ca-4e94-a0b6-e4674df22e4e",
  "parent_token_id": "8fe55f3b-7bd5-4768-a6df-6b28dbad1e61",
  "subaccount_id": "sa_7b1w9j2k4m8p",
  "scope": "withdraw_only",
  "expires_at": "2026-03-26T18:15:00Z",
  "spend_limit_usdc": 25,
  "delegation_depth": 1,
  "delegation_token": "satk_child_xxxxx"
}
```

***

## Freeze Sub-Account

```http theme={null}
POST /api/v1/subaccounts/{id}/freeze
```

Optional body:

```json theme={null}
{ "reason": "fraud-review" }
```

Freezing revokes active delegation tokens and blocks sub-account activity.

***

## Unfreeze Sub-Account

```http theme={null}
POST /api/v1/subaccounts/{id}/unfreeze
```

Optional body:

```json theme={null}
{ "reason": "manual-review-cleared" }
```

Guardrails:

* Only sub-accounts currently in `frozen` status can be unfrozen.
* `closed` sub-accounts cannot be unfrozen.
* Previously revoked delegation tokens are **not** reactivated. Mint new token(s) if needed.

***

## Drain Sub-Account

```http theme={null}
POST /api/v1/subaccounts/{id}/drain
```

Drains funds from the sub-account wallet back to the merchant wallet.

### Body

```json theme={null}
{
  "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

```http theme={null}
POST /api/v1/subaccounts/{id}/withdraw
```

### Body

```json theme={null}
{
  "to_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
  "amount": 10,
  "token": "Usdc",
  "mode": "live",
  "delegation_token": "satk_xxxxx",
  "signing_grant": "ssgt_xxxxx",
  "execution_intent_id": "89d52313-1cea-4313-80a4-23afc543287e"
}
```

`signing_grant` and `passkey_signature` are mutually exclusive.

`passkey_signature` remains available as an interactive fallback and is no longer required for headless external-withdraw flows.

***

## Withdraw Sub-Account To Bank (One-Shot)

```http theme={null}
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

```json theme={null}
{
  "amount_usdc": 25,
  "bank_id": "GTB",
  "account_number": "0123456789",
  "mode": "live",
  "signing_grant": "ssgt_xxxxx",
  "automation_token": "saatk_xxxxx",
  "delegation_token": "satk_xxxxx",
  "execution_intent_id": "89d52313-1cea-4313-80a4-23afc543287e"
}
```

`bank_id` accepts a bank identifier value: PAJ bank id, bank code, or bank name.

### Response

```json theme={null}
{
  "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.

`signing_grant` and `passkey_signature` are mutually exclusive.

For API-key initiated bank withdrawals, provide:

* one policy token (`automation_token` or `delegation_token`)
* one signing authorization (`signing_grant`)

`passkey_signature` remains available as an interactive fallback and is no longer required for headless flows.

***

## Mint Sub-Account Automation Token

```http theme={null}
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`.

`ttl_seconds` must be within the merchant effective automation-token TTL ceiling.

### Body

```json theme={null}
{
  "sub_account_id": "sa_7b1w9j2k4m8p",
  "ttl_seconds": 3600,
  "max_uses": 25,
  "total_limit_usdc": 500,
  "per_tx_limit_usdc": 50,
  "allowed_bank_ids": ["GTB"],
  "allowed_account_numbers": ["0123456789"],
  "mode": "live",
  "policy_version_id": "a6e9db6f-e264-4f9d-a4c7-7f94b18f0f34",
  "parent_token_id": "c4fab649-f4f0-438f-8dc8-9737a82c59d3",
  "agent_label": "reconciliation-agent",
  "agent_public_key": "ed25519:...",
  "agent_metadata": {
    "pipeline": "nightly-settlement"
  }
}
```

### Response

```json theme={null}
{
  "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": ["GTB"],
  "allowed_account_numbers": ["0123456789"],
  "mode": "live",
  "created_at": "2026-03-26T19:00:00Z"
}
```

<Warning>
  `automation_token` is returned once. Treat it as a secret and never log it.
</Warning>

***

## Mint Sub-Account Signing Grant

```http theme={null}
POST /api/v1/merchants/me/subaccounts/signing-grants
```

Requires merchant dashboard session auth. Use this endpoint to perform one interactive passkey approval and mint bounded headless signing credentials for `withdraw-bank`.

`ttl_seconds` must be within the merchant effective signing-grant TTL ceiling.

### Body

```json theme={null}
{
  "sub_account_id": "sa_7b1w9j2k4m8p",
  "ttl_seconds": 3600,
  "max_uses": 25,
  "total_limit_usdc": 500,
  "per_tx_limit_usdc": 50,
  "allowed_bank_ids": ["GTB"],
  "allowed_account_numbers": ["0123456789"],
  "mode": "live",
  "policy_version_id": "a6e9db6f-e264-4f9d-a4c7-7f94b18f0f34",
  "parent_grant_id": "e9ae9bc0-e640-4a2b-a7e8-d85b93b1a8a0",
  "active_days_utc": [1, 2, 3, 4, 5],
  "active_start_time_utc": "09:00",
  "active_end_time_utc": "18:00",
  "auto_renew": true,
  "agent_label": "ops-approver",
  "agent_public_key": "ed25519:...",
  "agent_metadata": {
    "team": "ops"
  },
  "passkey_signature": {
    "credential_id": "...",
    "authenticator_data": [1,2,3],
    "signature": [4,5,6],
    "client_data_json": [7,8,9]
  }
}
```

### Response

```json theme={null}
{
  "grant_id": "2f8fad5b-d9cb-469f-a165-70867728950e",
  "signing_grant": "ssgt_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": ["GTB"],
  "allowed_account_numbers": ["0123456789"],
  "mode": "live",
  "created_at": "2026-03-26T19:00:00Z"
}
```

<Warning>
  `signing_grant` is returned once. Treat it as a secret and never log it.
</Warning>

***

## Start Signing Grant Browser Intent

```http theme={null}
POST /api/v1/subaccounts/signing-grants/browser-intents/start
```

Recommended API-key flow for CLI/SDK parity with merchant dashboard UX. This creates a short-lived browser approval intent and returns a one-time approval URL.

`ttl_seconds` must be within the merchant effective signing-grant TTL ceiling.

### Body

```json theme={null}
{
  "sub_account_id": "sa_7b1w9j2k4m8p",
  "ttl_seconds": 3600,
  "max_uses": 25,
  "total_limit_usdc": 500,
  "per_tx_limit_usdc": 50,
  "allowed_bank_ids": ["GTB"],
  "allowed_account_numbers": ["0123456789"],
  "mode": "live"
}
```

### Response

```json theme={null}
{
  "intent_id": "f8df2a11-4f6b-4dbf-b5fb-d03ddcf3f2e3",
  "intent_token": "sgit_xxxxx",
  "approval_url": "https://api.zendfi.tech/api/v1/subaccounts/signing-grants/approval?intent_id=f8df2a11-4f6b-4dbf-b5fb-d03ddcf3f2e3&token=sgit_xxxxx",
  "expires_at": "2026-03-27T12:00:00Z"
}
```

<Warning>
  `intent_token` is returned once. Treat it as a secret and never log it.
</Warning>

***

## Poll Signing Grant Browser Intent

```http theme={null}
POST /api/v1/subaccounts/signing-grants/browser-intents/poll
```

Polls intent status. On approval, this returns the minted `signing_grant` exactly once and marks the intent consumed.

### Body

```json theme={null}
{
  "intent_id": "f8df2a11-4f6b-4dbf-b5fb-d03ddcf3f2e3",
  "intent_token": "sgit_xxxxx"
}
```

### Response (approved)

```json theme={null}
{
  "status": "approved",
  "completed": true,
  "expires_at": "2026-03-27T12:00:00Z",
  "grant": {
    "grant_id": "2f8fad5b-d9cb-469f-a165-70867728950e",
    "signing_grant": "ssgt_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": ["GTB"],
    "allowed_account_numbers": ["0123456789"],
    "mode": "live",
    "created_at": "2026-03-26T19:00:00Z"
  }
}
```

***

## Revoke Sub-Account Signing Grant

```http theme={null}
POST /api/v1/merchants/me/subaccounts/signing-grants/{grant_id}/revoke
```

Immediately revokes the signing grant.

### Response

```json theme={null}
{
  "success": true,
  "grant_id": "2f8fad5b-d9cb-469f-a165-70867728950e",
  "status": "revoked"
}
```

***

## Revoke Sub-Account Automation Token

```http theme={null}
POST /api/v1/merchants/me/subaccounts/automation-tokens/{token_id}/revoke
```

Immediately revokes the automation token.

### Response

```json theme={null}
{
  "success": true,
  "token_id": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "status": "revoked"
}
```

<Note>
  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.
</Note>

***

## Create Policy Version

```http theme={null}
POST /api/v1/merchants/me/subaccounts/policies
```

Creates a versioned policy document that can be attached to delegation tokens, automation tokens, signing grants, triggers, intents, and balance rules.

### Body

```json theme={null}
{
  "sub_account_id": "sa_7b1w9j2k4m8p",
  "policy_type": "signing_grant",
  "status": "active",
  "policy_json": {
    "max_per_tx_usdc": 100,
    "max_per_day_usdc": 500,
    "allowed_modes": ["live"],
    "allowed_weekdays_utc": [1, 2, 3, 4, 5],
    "active_start_utc": "09:00",
    "active_end_utc": "18:00"
  }
}
```

***

## Dry Run Policy

```http theme={null}
POST /api/v1/merchants/me/subaccounts/policies/dry-run
```

Evaluates a policy document without persisting it.

***

## Get Policy

```http theme={null}
GET /api/v1/merchants/me/subaccounts/policies/{policy_id}
```

***

## Create Webhook Trigger Subscription

```http theme={null}
POST /api/v1/merchants/me/subaccounts/webhook-triggers
```

Supported trigger types:

* `balance_below`
* `balance_above`
* `threshold_crossed`
* `funds_arrival`
* `daily_withdrawal_above`

***

## List Webhook Trigger Subscriptions

```http theme={null}
GET /api/v1/merchants/me/subaccounts/webhook-triggers
```

***

## Create Execution Intent

```http theme={null}
POST /api/v1/merchants/me/subaccounts/execution-intents
```

Creates a maker-checker style execution gate.

***

## Approve Execution Intent

```http theme={null}
POST /api/v1/merchants/me/subaccounts/execution-intents/{intent_id}/approve
```

***

## Release Execution Intent by Signal

```http theme={null}
POST /api/v1/subaccounts/execution-intents/release
```

Releases a pending or approved intent using its one-time signal token.

***

## Create Balance Rule

```http theme={null}
POST /api/v1/merchants/me/subaccounts/balance-rules
```

Creates an automated balance action rule (`topup_below` or `drain_above`).

***

## Close Sub-Account

```http theme={null}
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`, `SubAccountUnfrozen`, `SubAccountClosed`
* Reactive controls: `SubAccountBalanceLow`, `SubAccountBalanceHigh`, `SubAccountThresholdCrossed`
* Execution gates: `SubAccountExecutionGatePending`, `SubAccountExecutionGateReleased`

See [Webhooks](/api-reference/webhooks) for payload structure.
