Skip to main content

API Conventions

Five minutes here prevents most integration bugs. These rules apply to every endpoint.

Errors

Non-2xx responses always carry the same envelope:

{
"error": {
"code": "below_min_deposit",
"message": "Minimum deposit is 1 USDC",
"requestId": "req_01h2x…"
}
}

Quote requestId in support requests. Error codes:

CodeHTTPMeaning
invalid_request400Malformed body, unknown field value, violated constraint
authentication_error401Bad/missing/revoked key
permission_denied403Key type or account state forbids this action (frozen account, missing KYC/KYB attestation, treasury approver = creator)
not_whitelisted403Wallet not enrolled for a whitelist-gated vault (e.g. xaua-sui) — deposit builds run an on-chain preflight
not_found404Resource doesn't exist or belongs to another organization
conflict409Uniqueness violated (wallet already bound, externalId taken, digest already linked)
idempotency_conflict409Same Idempotency-Key, different request body
idempotency_in_progress409Same Idempotency-Key while the original request is still executing — carries Retry-After: 2; retry with the same key
vault_paused409Vault not accepting new deposits/redemptions (e.g. the frozen legacy Solana rail); claim and cancel stay open
cycle_closed409Reserved — defined in the contract but not currently emitted; cycle-window conflicts surface as on-chain aborts at execution
rate_limited429Slow down; honor Retry-After
below_min_deposit400Amount under the vault's minDeposit
insufficient_balance400Nothing claimable, not enough USDC, or the wallet has no gas coin to build the transaction
insufficient_shares400Redeem amount exceeds available shares
upstream_unavailable502An upstream chain RPC request failed — currently only the legacy Solana reads, which proxy a public devnet RPC. Retryable; prefer the current /v1/vaults and /v1/accounts reads
internal_error500Our fault; safe to retry with the same idempotency key

Request IDs

Every response — success or error — carries an x-request-id header; on errors the same id appears as requestId in the envelope. Send your own x-request-id (8–64 chars of A-Z a-z 0-9 _ -) and it is echoed back, correlating your logs with ours end-to-end; otherwise the gateway mints one.

Idempotency

Every money-movement POST accepts an Idempotency-Key header (any unique string ≤255 chars; UUIDs recommended):

  • Same key + same body → the original response is replayed verbatim (no duplicate effect), marked with an Idempotency-Replayed: true header.
  • Same key + different body → 409 idempotency_conflict.
  • Same key while the original request is still executing (concurrent duplicate) → 409 idempotency_in_progress with Retry-After: 2. Exactly one execution happens — the key is claimed before the handler runs. Retry with the same key to collect the replayed response; @tbookdev/vault-node ≥ 0.2.1 does this automatically.
  • Keys are retained for 24 hours — a same-key retry after that re-executes the request. Don't rely on idempotency for retries spanning more than a day.
  • Storage is scoped to your organization. Anonymous (keyless) requests are not deduplicated — use an API key for any money movement you may retry.
  • 5xx responses are not cached, so you can safely retry them with the same key.

Use it on: all tx/* builds, account creation, treasury withdrawals, webhook registration. If a build's transaction expires unsigned, rebuild with a new key — the old intent simply expires harmlessly.

Amounts

All monetary values are decimal strings ("100.50"), never floats, with explicit currency/asset context. Shares use the same convention. Parse with a decimal library, not parseFloat.

Pagination

List endpoints take ?limit= (default 20, max 100) and ?cursor=:

{ "data": [], "nextCursor": "eyJpZCI6…", "hasMore": true }

Pass nextCursor back verbatim — cursors are opaque. One exception: GET /v1/events paginates with after + types + limit (default 50, max 200) instead of a cursor.

Timestamps & IDs

Timestamps are ISO-8601 UTC (2026-06-15T08:14:05Z). IDs carry type prefixes: acct_, txi_ (transaction intent), al_ (allowlist entry), twd_ (treasury withdrawal), wh_ (webhook), evt_ (event), req_ (request id in errors).

Versioning & forward compatibility

  • The API is versioned by path: /v1. Breaking changes mean /v2 with at least 6 months of overlap.
  • Additive changes happen at any time without notice: new fields, new webhook event types, new enum values (e.g. new asset types as the catalog grows). Parse leniently — ignore unknown fields, never reject unknown enum values, and route unknown webhook event types to a no-op handler.

Environments

Sandbox (rwa-api-sandbox.tbook.com) and production (rwa-api.tbook.com) are wire-compatible. Sandbox adds two extra endpoints under /v1/sandbox/* that do not exist in production.