Custody API

Transactions

A transaction is an intent that flows through network authorization, policy, screening, and quota checks before it can be signed and broadcast. Native and QRC20 transfers use the same endpoint; token routing is automatic from the selected assetId.

Create a transfer

POST/v1/transactions

{
  "externalId": "payout-2026-08-001",   // your idempotency handle (see below)
  "networkCode": "QUANTUM_SANDBOX",     // required explicit operation network
  "sourceWalletId": "…",                // required — must not be WATCH_ONLY
  "destinationAddress": "0x…",          // required
  "assetId": "20804000-0000-4000-8000-000000000001", // required; QRC20 assets route to token transfer
  "amount": "1000000000000000000",      // required; decimal integer string in Qwei/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

stateDiagram-v2
    [*] --> SUBMITTED
    SUBMITTED --> QUEUED
    QUEUED --> REJECTED: policy denies
    QUEUED --> PENDING_AUTHORIZATION: approval rule matches
    PENDING_AUTHORIZATION --> PENDING_SIGNATURE: approve
    PENDING_AUTHORIZATION --> REJECTED: reject
    QUEUED --> PENDING_AML_SCREENING: screening pending / flagged
    PENDING_AML_SCREENING --> PENDING_SIGNATURE: clears
    PENDING_AML_SCREENING --> BLOCKED: blocked
    QUEUED --> PENDING_SIGNATURE: all checks clear
    PENDING_SIGNATURE --> SIGNED: approved in EnQlave
    PENDING_SIGNATURE --> FAILED: signing failure / expiry
    SIGNED --> BROADCASTING
    BROADCASTING --> CONFIRMING
    BROADCASTING --> FAILED: broadcast failure
    CONFIRMING --> COMPLETED
    COMPLETED --> [*]
  

Any pre-broadcast state can be moved to CANCELLED via the cancel endpoint. 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": "20804000-0000-4000-8000-000000000001",
  "status": "CONFIRMING", "failureReason": "",
  "chainTxHash": "0x…", "confirmations": 3,
  "amount": "1000000000000000000",            // decimal strings, present when set:
  "fee": "…", "gasPrice": "…", "maxFeePerGas": "…", "maxPriorityFee": "…",
  "gasLimit": 21000, "nonce": 17, "blockNumber": 3392847,
  "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 Contract interactions are exposed through typed, policy-controlled contract and tokenization endpoints. Raw arbitrary calldata is not accepted for deployment; Qustody encodes constructor/function arguments against verified ABI evidence.