Custody API

Transactions

A transaction is an intent that flows through policy, screening, and quota checks before it can be signed and broadcast. Native transfers and token (ERC-20/QRC-20) transfers use the same endpoint — token routing is automatic based on assetId.

Create a transfer

POST/v1/transactions

{
  "externalId": "payout-2026-08-001",   // your idempotency handle (see below)
  "sourceWalletId": "…",                // required — must not be WATCH_ONLY
  "destinationAddress": "0x…",          // required
  "assetId": "QC",                      // required; token assets route via ERC-20 transfer
  "amount": "1000000000000000000",      // required — decimal integer string, wei / raw units
  "note": "August payout batch 1",
  "metadata": {"batch": "2026-08"}
}

Returns 201 with the full transaction object in status SUBMITTED.

Idempotency — use both layers

Header name The header is X-Idempotency-Key — not Idempotency-Key. The latter is silently ignored.

Status lifecycle

SUBMITTED ──▶ QUEUED │ ├─ policy denies ────────────────────▶ REJECTED ◉ ├─ approval rule matches ───────────▶ PENDING_AUTHORIZATION ─ approve ─▶ PENDING_SIGNATURE │ └──── reject ────▶ REJECTED ◉ ├─ screening pending / flagged ─────▶ PENDING_AML_SCREENING ─ clears ──▶ PENDING_SIGNATURE │ └─── blocked ───▶ BLOCKED ◉ └─ all checks clear ────────────────▶ PENDING_SIGNATURE │ your signer submits a valid signature ▼ SIGNED ─▶ BROADCASTING ─▶ CONFIRMING ─▶ COMPLETED ◉ │ └─ broadcast / signing failure ─▶ FAILED ◉ any pre-broadcast state ── cancel ─▶ CANCELLED ◉ ◉ = terminal

COMPLETEDFAILEDREJECTEDCANCELLEDBLOCKED are terminal. PENDING_AML_SCREENING is also the fail-safe parking state when the screening provider itself errors — treat it as "on hold", not strictly "screening in progress".

The transaction object

{
  "id": "…", "tenantId": "…", "externalId": "payout-2026-08-001",
  "sourceVaultId": "…", "sourceWalletId": "…",
  "destinationAddress": "0x…", "assetId": "QC",
  "status": "CONFIRMING", "failureReason": "",
  "chainTxHash": "0x…", "confirmations": 3,
  "amount": "1000000000000000000",            // decimal strings, present when set:
  "fee": "…", "gasPrice": "…", "maxFeePerGas": "…", "maxPriorityFee": "…",
  "gasLimit": 21000, "nonce": 17, "blockNumber": 3392847,
  "signingHash": "ab34…",                     // hex, no 0x — only while PENDING_SIGNATURE
  "note": "…", "metadata": {…},
  "createdAt": "…", "updatedAt": "…", "broadcastAt": "…", "confirmedAt": "…"
}

Query, estimate, act

EndpointPurpose
GET/v1/transactionsList. Filters: status, vaultId, walletId, assetId, dateFrom, dateTo (RFC 3339, inclusive); sorting: sortBy=createdAt&sortOrder=asc|desc; paging: limit (default 20, max 100) + cursor
GET/v1/transactions/{id}Fetch one
GET/v1/transactions/exportCSV export (same filters)
GET/v1/transactions/estimate-fee?sourceAddress=…&destinationAddress=…&assetId=…&amount=…Fee estimate before creating
POST/v1/transactions/{id}/approveBody {"approverId","reason"} — moves to PENDING_SIGNATURE
POST/v1/transactions/{id}/rejectBody {"approverId","reason"}
POST/v1/transactions/{id}/cancelCancel any pre-broadcast transaction

Cursor pagination

List responses end with "page": {"nextCursor": "…", "hasMore": true}. Cursors are opaque and direction-bound — replaying a cursor under a different sortOrder returns 400 invalid cursor. Iterate until hasMore is false.

Contract calls There is no generic contract-call endpoint. Contract interactions (token deployment, mint, burn, pause) are exposed through the dedicated tokenization endpoints under /v1/tokenization, which create the corresponding transactions internally and run them through the same lifecycle.