Account Endpoints¶
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 = falsemeans 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¶
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:
POST /approveto build an ERC-20 allowance tx when you are not using permit.POST /depositto builddepositFund(...)ordepositFundWithPermit(...).POST /tx/sendto 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¶
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¶
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¶
Authentication: API Key required
Request Body¶
Response¶
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¶
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¶
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¶
Authentication: Optional
Request Body¶
Response¶
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:
- Call
emergencyWithdraw(token, amount)with a positive amount covered by your current Vault balance to create a request. - Wait at least 7,200 blocks (about 24 hours at 12 seconds per block).
- 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. - 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¶
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¶
Authentication: API Key required
Request Body¶
Response¶
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 /transferrequiresfrom_addressto match the API-key owner./transfer/sendaccepts onlytransfer(...)calls to registered tokens, and the raw transaction signer must match that owner./tx/sendlikewise 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/sendrequires the raw transaction signer to equalintent.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_hashconfirms submission to the RPC, not successful mining or a final balance change; check the transaction receipt and refreshed balances.