Skip to main content

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

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"
}

Get Merchant Sub-Account TTL Policy

GET /api/v1/subaccounts/ttl-policy
Returns effective TTL ceilings, merchant overrides, and platform hard caps.

Response

{
  "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

POST /api/v1/subaccounts/ttl-policy
Updates merchant-specific TTL ceilings. Each provided value must be within the platform hard-cap range.

Body

{
  "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

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.
policy_version_id
string
Optional policy version UUID attached to this token.
agent_label
string
Optional actor label for audit attribution.
agent_public_key
string
Optional actor public key for attribution.
agent_metadata
object
Optional structured actor metadata.

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.

Mint Child Delegation Token

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

{
  "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

{
  "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

POST /api/v1/subaccounts/{id}/freeze
Optional body:
{ "reason": "fraud-review" }
Freezing revokes active delegation tokens and blocks sub-account activity.

Unfreeze Sub-Account

POST /api/v1/subaccounts/{id}/unfreeze
Optional body:
{ "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

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",
  "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)

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": "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

{
  "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

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

{
  "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

{
  "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"
}
automation_token is returned once. Treat it as a secret and never log it.

Mint Sub-Account Signing Grant

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

{
  "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

{
  "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"
}
signing_grant is returned once. Treat it as a secret and never log it.

Start Signing Grant Browser Intent

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

{
  "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

{
  "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"
}
intent_token is returned once. Treat it as a secret and never log it.

Poll Signing Grant Browser Intent

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

{
  "intent_id": "f8df2a11-4f6b-4dbf-b5fb-d03ddcf3f2e3",
  "intent_token": "sgit_xxxxx"
}

Response (approved)

{
  "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

POST /api/v1/merchants/me/subaccounts/signing-grants/{grant_id}/revoke
Immediately revokes the signing grant.

Response

{
  "success": true,
  "grant_id": "2f8fad5b-d9cb-469f-a165-70867728950e",
  "status": "revoked"
}

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.

Create Policy Version

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

{
  "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

POST /api/v1/merchants/me/subaccounts/policies/dry-run
Evaluates a policy document without persisting it.

Get Policy

GET /api/v1/merchants/me/subaccounts/policies/{policy_id}

Create Webhook Trigger Subscription

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

GET /api/v1/merchants/me/subaccounts/webhook-triggers

Create Execution Intent

POST /api/v1/merchants/me/subaccounts/execution-intents
Creates a maker-checker style execution gate.

Approve Execution Intent

POST /api/v1/merchants/me/subaccounts/execution-intents/{intent_id}/approve

Release Execution Intent by Signal

POST /api/v1/subaccounts/execution-intents/release
Releases a pending or approved intent using its one-time signal token.

Create Balance Rule

POST /api/v1/merchants/me/subaccounts/balance-rules
Creates an automated balance action rule (topup_below or drain_above).

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, SubAccountUnfrozen, SubAccountClosed
  • Reactive controls: SubAccountBalanceLow, SubAccountBalanceHigh, SubAccountThresholdCrossed
  • Execution gates: SubAccountExecutionGatePending, SubAccountExecutionGateReleased
See Webhooks for payload structure.