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:
| Code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | Malformed body, unknown field value, violated constraint |
authentication_error | 401 | Bad/missing/revoked key |
permission_denied | 403 | Key type or account state forbids this action (frozen account, missing KYC/KYB attestation, treasury approver = creator) |
not_whitelisted | 403 | Wallet not enrolled for a whitelist-gated vault (e.g. xaua-sui) — deposit builds run an on-chain preflight |
not_found | 404 | Resource doesn't exist or belongs to another organization |
conflict | 409 | Uniqueness violated (wallet already bound, externalId taken, digest already linked) |
idempotency_conflict | 409 | Same Idempotency-Key, different request body |
idempotency_in_progress | 409 | Same Idempotency-Key while the original request is still executing — carries Retry-After: 2; retry with the same key |
vault_paused | 409 | Vault not accepting new deposits/redemptions (e.g. the frozen legacy Solana rail); claim and cancel stay open |
cycle_closed | 409 | Reserved — defined in the contract but not currently emitted; cycle-window conflicts surface as on-chain aborts at execution |
rate_limited | 429 | Slow down; honor Retry-After |
below_min_deposit | 400 | Amount under the vault's minDeposit |
insufficient_balance | 400 | Nothing claimable, not enough USDC, or the wallet has no gas coin to build the transaction |
insufficient_shares | 400 | Redeem amount exceeds available shares |
upstream_unavailable | 502 | An 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_error | 500 | Our 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: trueheader. - Same key + different body →
409 idempotency_conflict. - Same key while the original request is still executing (concurrent duplicate) →
409 idempotency_in_progresswithRetry-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.
5xxresponses 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/v2with 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.