Skip to content

Account Endpoints

Get Balances

GET /balances

Authentication: API Key required

Query Parameters

Parameter Type Required Description
owner_address address Yes Must match the authenticated API-key owner
include_zero boolean No Defaults to false: omits ordinary zero-total rows, but keeps zero rows with recovery/sync/restriction state. true includes every whitelisted token.

Response

{
  "owner_address": "0x...",
  "balances": [
    {
      "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
      "symbol": "EURC",
      "decimals": 6,
      "wallet_balance": "1250000000",
      "vault_available": "400000000",
      "vault_frozen": "100000000",
      "vault_total": "500000000",
      "total": "1750000000",
      "chain_verified": null,
      "spendability_status": null,
      "restriction_reason": null,
      "recovery_status": null,
      "projection_present": null,
      "recovery_version": null,
      "balance_sync_hint": null,
      "coverage_block_number": null,
      "coverage_block_hash": null,
      "coverage_checked_at": null,
      "verification_status": null,
      "generation": null,
      "snapshot_block_number": null,
      "snapshot_block_hash": null,
      "snapshot_policy_digest": null,
      "projection_updated_at": null,
      "vault_spendable": null
    }
  ],
  "updated_at": "2026-04-15T09:12:34.567890+00:00",
  "wallet_balance_available": true,
  "vault_balance_available": true
}
Field Type Description
token address ERC-20 contract address
symbol string Token ticker
decimals integer Token precision; use to convert the raw strings to human-readable values
wallet_balance string User's wallet balance (raw uint256 decimal string)
vault_available string Available committed accounting balance, not necessarily spendable (raw uint256 string).
vault_frozen string Vault balance locked in open orders (raw uint256 decimal string)
vault_total string vault_available + vault_frozen
total string wallet_balance + vault_total
wallet_balance_available boolean false if the wallet RPC lookup failed

Monetary balance fields are raw uint256 decimal strings. Convert using decimals; interpret eligibility and observation availability separately.

Response Notes

  • wallet_balance_available = false means wallet lookup failed and wallet amounts are "0"; it does not establish Vault spending eligibility.
  • vault_available + vault_frozen = vault_total.
  • wallet_balance + vault_total = total.

Spendability and recovery

Use vault_spendable, when non-null, for the reported spendable amount. vault_available is the available amount in committed accounting; recovery or restrictions can leave it positive while vault_spendable is "0". This read does not authorize an order: final admission rechecks eligibility. In legacy balance mode the new per-asset state fields can be null; do not interpret null as zero or as proof of eligibility.

Field Type Meaning
vault_spendable string / null Spendable raw amount after qualification; can be zero despite a positive accounting balance.
spendability_status string / null Eligibility state, including ready, local_covered, restricted, sync_required, current_coverage_required.
restriction_reason string / null Reason for the restriction; null when no reason is reported.
recovery_status string / null Recovery state, including ready, pending, draining, recovering, proven, blocked.
projection_present boolean / null Whether a committed accounting projection exists for this asset.
recovery_version string / null Opaque recovery version; pass the current value to balance sync.
balance_sync_hint string / null none: no observed difference; check_required: chain/accounting difference; unknown: no fresh chain observation. This is not spend authorization.
coverage_block_number / coverage_block_hash integer / string / null Block identity for current coverage evidence.
coverage_checked_at string / null ISO timestamp of coverage verification; cached responses do not renew this proof.
verification_status string / null Accounting snapshot verification state, such as verified or unverified; not a standalone spending decision.
generation integer / null Accounting snapshot generation.
snapshot_block_number / snapshot_block_hash integer / string / null Block identity of the accounting snapshot.
snapshot_policy_digest string / null Policy digest associated with the snapshot.
projection_updated_at string / null ISO timestamp of the accounting projection update; not the chain tip or a fresh verification time.

Outer vault_balance_available reports whether the Vault chain observation is available and fresh, even when balances=[]; it is separate from snapshot verification_status. A local_covered row may have spendable funds with historical verification_status="unverified". projection_updated_at is not proof freshness. Unknown states should trigger a refresh, not an assumption that funds are spendable.

For example, vault_available="400000000" with vault_spendable="0", spendability_status="restricted" and recovery_status="recovering" is a valid restricted balance. Zero rows are retained for recovery states pending/draining/recovering/proven/blocked, restricted/sync_required/current_coverage_required eligibility, balance_sync_hint="check_required", or a present projection with an unknown sync hint. See balance sync for recovery.

Example

import requests

balances = requests.get(
    "https://api.testnet.sera.cx/api/v1/balances",
    params={"owner_address": "0x..."},
    headers={"Authorization": "Bearer sera_key:secret"},
    timeout=10,
).json()["balances"]

for bal in balances:
    print(f"{bal['symbol']}: available={bal['vault_available']}, frozen={bal['vault_frozen']}")
const response = await fetch(
  "https://api.testnet.sera.cx/api/v1/balances?owner_address=0x...",
  { headers: { "Authorization": "Bearer sera_key:secret" } },
);
const { balances } = await response.json();

for (const bal of balances) {
  console.log(`${bal.symbol}: available=${bal.vault_available}, frozen=${bal.vault_frozen}`);
}

Request balance synchronization

POST /api/v1/balances/sync
POST /api/v2/balances/sync

Authentication: API Key required; owner_address must match its owner. Check GET /api/v1/config → capabilities.balances.sync_enabled first. This is an accounting synchronization request, not a deposit or payment.

Request

{
  "owner_address": "0x1111111111111111111111111111111111111111",
  "token_address": "0x965d4b4546716e416e950bc30467d128455d2d0e",
  "expected_recovery_version": null
}

owner_address and token_address are 0x-prefixed 20-byte hex address strings, normalized to lowercase. All three fields are required; extra fields are rejected. Read balances first and send that asset's recovery_version as expected_recovery_version: explicitly null when no version exists, otherwise a 64-character lowercase hex string. Do not omit it or invent a version.

Response

HTTP 202:

{
  "status": "queued",
  "recovery_status": "pending",
  "recovery_version": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}

status is queued or already_pending; recovery_status is pending, draining, recovering or proven. Poll GET balances to observe progress and spending eligibility; 202 and proven alone do not authorize a trade. Do not deposit again to resolve synchronization delay.

HTTP detail.code / action
400 unknown_token: use a token from the registry
409 balance_sync_unavailable: this deployment does not currently accept sync
409 balance_recovery_blocked: inspect the returned reason and balance state; do not retry blindly
409 balance_sync_version_changed: reread balances and decide whether to retry with the current version

Errors may include detail.recovery_version (nullable) and detail.reason: vault_below_frozen, frozen_reservation_differs, chain_event_conflict, chain_snapshot_integrity_error or account_recovery_blocked. Also handle 401/403, 422, 429 and generic 503. The request belongs to the transfer rate-limit group (currently 2 req/s). On an unknown response, reread balances before retrying; preserve the submitted version until reconciled.

Deposit Helpers

Fetch the live vault_address and sor_address from GET /config. Deposits now use three helpers:

  1. POST /approve to build an ERC-20 allowance tx when you are not using permit.
  2. POST /deposit to build depositFund(...) or depositFundWithPermit(...).
  3. POST /tx/send to broadcast the signed approve or deposit tx.

If the token supports EIP-2612, you can skip a separate approve by passing permit_signature and permit_deadline to POST /deposit. For client-side permit discovery, use GET /permit/metadata.

Deposit and withdrawal availability

Read /config before showing actions: capabilities.deposits.enabled, .deposits.restriction_reason, .withdrawals.instant_enabled and .balances.sync_enabled. When deposit accounting is paused, /deposit and the deposit branch of /tx/send return 409 with detail.code="deposit_accounting_paused", including already signed deposits awaiting broadcast. Do not create another deposit; wait for the capability to recover. This guard does not describe the approve branch of /tx/send.

Build Deposit Transaction

POST /deposit

Authentication: API Key required

Request Body

{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "owner": "0x...",
  "amount": "1000000",
  "permit_signature": "0x...",
  "permit_deadline": 1713254400,
  "permit_amount": "1000000"
}
Field Type Required Description
token address Yes ERC-20 token address to deposit
owner address Yes Depositor address; must match the authenticated API-key owner
amount string Yes Raw uint256 token amount
permit_signature hex string No EIP-2612 permit signature for inline allowance grant; both standard 65-byte and compact 64-byte hex encodings are accepted
permit_deadline integer No Deadline signed into the permit
permit_amount string No Raw uint256 permit allowance; defaults to amount when omitted

When permit fields are omitted, the builder targets depositFund(token, owner, amount). When permit fields are present, it targets depositFundWithPermit(...) instead.

Response

{
  "tx": {
    "to": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2b",
    "gas": "0x13880",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}

Build Approve Transaction

POST /approve

Authentication: API Key required

Request Body

{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "owner": "0x...",
  "spender": "0x3c7945840bAE0d7e7f3824Ebccef1962629250F0",
  "amount": "1000000"
}

spender must be the live Vault or SOR address from GET /config; other targets are rejected.

Response

{
  "tx": {
    "to": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2a",
    "gas": "0xc350",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}

Broadcast Signed Approve Or Deposit

POST /tx/send

Authentication: API Key required

Request Body

{
  "raw_tx": "0x..."
}

Response

{
  "tx_hash": "0x..."
}

POST /tx/send only accepts signed approve / deposit calls that match an allowed selector-to-target pairing.

For approve, the encoded spender must also be the live Vault or SOR address from GET /config. If you submit an EIP-7702 type-4 transaction, every authorization delegate must be on the deployment's allowlist or the request is rejected.

Withdraw Co-Signature

Instant withdrawal is a dual-signature flow: the user signs WithdrawIntent, the executor co-signs it, and the user then signs the final transaction.

1. Request Executor Co-Signature

POST /withdraw

Authentication: Optional. If an API key is supplied, intent.user must match the authenticated owner.

Request Body

{
  "intent": {
    "user": "0x...",
    "tokens": ["0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f"],
    "amounts": ["1000000"],
    "recipient": "0x...",
    "deadline": "1713254400",
    "uuid": "123456789"
  },
  "user_signature": "0x..."
}
Field Type Description
intent.user address Withdrawing user's wallet
intent.tokens address[] Token addresses to withdraw (1-20)
intent.amounts string[] Per-token amounts (raw uint256 strings)
intent.recipient address Destination wallet for the withdrawn tokens
intent.deadline string Unix seconds as a uint256 string. For POST /withdraw, it must be in the future and no more than 600 seconds ahead; read limits.withdraw.cosignature_max_ttl_seconds from GET /config.
intent.uuid string Unique replay-protection identifier (uint256 as string)
user_signature string EIP-712 WithdrawIntent signature

tokens and amounts must have the same length, with 1 to 20 entries. Each amount must be a positive uint256 string.

Response

{
  "success": true,
  "executor_address": "0xDa6e605DB8c3221f4B3706c1da9C4E28195045f5",
  "executor_signature": "0x...",
  "error": null
}

2. Build Withdrawal Transaction

POST /withdraw/build

Authentication: Optional. If an API key is supplied, intent.user must match the authenticated owner.

Request Body

{
  "intent": {
    "user": "0x...",
    "tokens": ["0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f"],
    "amounts": ["1000000"],
    "recipient": "0x...",
    "deadline": "1713254400",
    "uuid": "123456789"
  },
  "user_signature": "0x...",
  "executor": "0xDa6e605DB8c3221f4B3706c1da9C4E28195045f5",
  "executor_signature": "0x..."
}
Field Type Description
intent object Same WithdrawIntent as step 1
user_signature string Your EIP-712 signature
executor address Co-signing executor address from step 1's response
executor_signature string Executor co-signature returned by step 1

Response

{
  "tx": {
    "to": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2c",
    "gas": "0x249f0",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}

3. Broadcast Signed Withdrawal

POST /withdraw/send

Authentication: Optional

Request Body

{
  "raw_tx": "0x..."
}

Response

{
  "tx_hash": "0x..."
}

POST /withdraw/send only accepts signed executeInstantWithdrawDualSig(...) calls targeting the live Sera contract from GET /config. If you submit an EIP-7702 type-4 transaction, every authorization delegate must be on the deployment's allowlist or the request is rejected.

Emergency Withdrawal

If the API is unavailable, use Sera's on-chain two-step flow:

  1. Call emergencyWithdraw(token, amount) with a positive amount covered by your current Vault balance to create a request.
  2. Wait at least 7,200 blocks (about 24 hours at 12 seconds per block).
  3. Execute from request block + 7,200 through + 14,400 inclusive, using the same token and 0 < amount <= requested amount. Your current balance must still cover it.
  4. Success deletes the entire request, even for a partial withdrawal. After the 14,400-block window, a call starts a new request and delay.

See the Sera.sol contract reference for the full rules.

ERC-20 Transfer

The API can also build and broadcast direct ERC-20 transfers for whitelisted tokens.

Build Transfer

POST /transfer

Authentication: API Key required

Request Body

{
  "token": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
  "to": "0x...",
  "amount": "1000000",
  "from_address": "0x..."
}
Field Type Required Description
token address Yes ERC-20 token contract address (must be in the token registry)
to address Yes Recipient address
amount string Yes Amount in raw token units
from_address address Yes Sender wallet address

Response

{
  "tx": {
    "to": "0xef64d15ed6c371545eb6dcd6c026c17dfb6c440f",
    "data": "0x...",
    "value": "0x0",
    "chainId": "0xaa36a7",
    "nonce": "0x2d",
    "gas": "0xea60",
    "type": "0x2",
    "maxFeePerGas": "0x...",
    "maxPriorityFeePerGas": "0x..."
  }
}

Broadcast Transfer

POST /transfer/send

Authentication: API Key required

Request Body

{
  "raw_tx": "0x..."
}

Response

{
  "tx_hash": "0x..."
}

Balance Verification and Withdrawal Errors

chain_verified=true describes a legacy-mode chain correction of cached Vault amounts. It does not describe committed accounting verification; use the state fields above. Null does not prove a fresh check. Inspect wallet_balance_available, vault_balance_available and updated_at separately.

GET /api/v2/balances is an alias with the same query parameters and response. All balance amounts are raw integer strings.

POST /withdraw returns executor_address together with executor_signature. Pass that address as the required executor in /withdraw/build. The co-signature deadline is limited to 600 seconds, even though the general signed-intent model permits a longer window. A 409 with detail.error_code="WITHDRAW_UNAVAILABLE" means the instant-withdraw gate rejected the request; refresh balances/orders before retrying. Protected account actions can also return COMPLIANCE_BLOCKED.

Transaction Signing and Broadcast Rules

  • POST /transfer requires from_address to match the API-key owner. /transfer/send accepts only transfer(...) calls to registered tokens, and the raw transaction signer must match that owner.
  • /tx/send likewise binds the outer signer to the API-key owner. For deposits, the encoded deposit owner must also match. A deposit permit is rechecked against its token, Vault spender, amount, current nonce and deadline before broadcast; a stale permit requires fresh metadata and a new signature.
  • /withdraw/send requires the raw transaction signer to equal intent.user; a third-party payer cannot replace that signer. If a key is supplied, its owner must match as well. The user and recipient are screened again before broadcast.
  • Builders return unsigned transactions. A returned tx_hash confirms submission to the RPC, not successful mining or a final balance change; check the transaction receipt and refreshed balances.