Skip to main content

Transactions

Transaction Lifecycle

Every transaction follows a defined state machine from creation to on-chain confirmation (or terminal failure):

StatusDescriptionNext
pendingCreated; no quorum required (or quorum already satisfied at creation time).requested, to-cancel, failed
approval-pendingWaiting for required vault-quorum approvals.approved, to-cancel, cancelled
approvedQuorum reached; waiting to be sent to signing.requested, to-cancel
requestedQueued for signing.to-sign, signed, to-cancel, failed
to-signExternal-signer vaults only — payloads prepared, awaiting your signatures (see External-signer signing).signed, to-cancel
signedSignature complete.submitted, failed
submittedBroadcast to the blockchain.mined, replaced, failed
minedConfirmed on chain (terminal — happy path).
to-cancelCancellation requested via Decline; platform is unwinding.cancelled
cancelledCancelled (terminal — see reasonCode on the event for details).
failedFailed on submission/processing (terminal).
replacedSuperseded by a replacement transaction that mined (terminal).

A warning event (separate from status) is emitted when processing is paused but the transaction is still alive — most commonly for insufficient balance at signing time. See Warnings and retries. For the full set of lifecycle events pushed to your endpoint, see Webhooks — Outgoing stream.

Cancellation reason codes you may see on cancelled events (webhooks and API): insufficient_balance, singner_unavailable, hosted_singner_unavailable, managed_singner_unavailable, shared_singner_unavailable, network_maintenance, not_enough_utxos, order_expired, invalid_order, transaction_expired, declined_by_client, rejected_by_approver, rejected_by_master_approver, replacement_status_invalid, hash_already_exists.

Create a Transaction

tip

Asset IDs The asset field accepts a Universal Asset ID, not a human-readable name. Query available assets via GET /networks/{network}/assets. Native coins use a SLIP-44–scoped identifier (e.g. c60 for native ETH on Ethereum, c0 for native BTC); token assets append a _t… segment that encodes the token contract or issuer (e.g. c60_t0xdac17f958d2ee523a2206206994597c13d831ec7 for USDT on Ethereum).

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"addressId": "gMP71sR5sNUnGdKFTsNzp6",
"destination": "0x742d35Cc6634C0532925a3b8...",
"network": "ethereum-mainnet",
"asset": "c60_t0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "1000.50",
"feePriority": "medium",
"note": "Payment to supplier"
}' \
https://api.carabaas.com/api/v1/transactions

Request fields

FieldRequiredDescription
orderIdYesA valid UUID string. Idempotency key — see Idempotency.
addressIdConditionalSource address. Required for non-UTXO networks. For UTXO networks one of addressId / accountId / vaultId must be provided.
accountIdConditionalSource account (UTXO chains only) — collect UTXOs across all addresses in the account.
vaultIdConditionalSource vault (UTXO chains only) — collect UTXOs across all addresses in the vault.
destinationYesRecipient on-chain address. Alphanumeric string, 12–200 characters ([a-z0-9], case-insensitive). Canonicalized to the network’s wire format when applicable.
networkYesCustody network identifier (e.g. ethereum-mainnet, bitcoin-mainnet).
assetYesUniversal Asset ID for that network.
amountYesDecimal string in the asset’s human units (e.g. "1000.50").
feePriorityNoOne of low | medium | high. Default medium. (custom is not accepted — use options.gasPrice on ETH-like networks instead.)
noteNoUp to 256 chars. Visible only to you — saved with the transaction record.
referenceNoUp to 128 chars. Shared field — visible to you and to the counterparty integrations.
optionsNoNetwork-specific knobs — see below.

options

OptionApplies toDescription
gasPriceETH-likeDecimal string. Takes precedence over feePriority when set.
gasLimitETH-likeDecimal string.
dataETH-likeHex-encoded calldata (0x…) for smart-contract interaction.
memoXRP, ATOM, EOS, HBAR, LUNA/LUNC, XDB, XEM, XLM, ALGODestination tag / memo. Wire format depends on the chain.
changeAddressIdUTXOAddress that should receive the change output. Must belong to the source vault.
feePayerAddressIdSolanaAddress paying the network fee. Must belong to the source vault.

Source selection

Network familyRequired source fields
Account-based (ETH-like, Tezos, Solana, Tron, Cosmos, Ripple, Stellar, …)addressId is required. On Tezos, the source address must already be revealed — otherwise create returns 400 Address is not revealed.
UTXO (Bitcoin, Litecoin, Dogecoin, Bitcoin Cash, Dash)One of addressId / accountId / vaultId. When sending from accountId / vaultId, also pass options.changeAddressId.

For UTXO specifics — change handling, address types — see Bitcoin & UTXO Chains.

If you pass more than one source field, the platform validates they refer to the same vault/account; mismatches return 400.

Authorization

The caller must hold:

  • read on the organization that owns the vault
  • initiate on the vault that owns the source address

Without either permission the request is rejected with 403 before validation.

Response

On success returns a Transaction object. Read the HTTP status to distinguish create vs idempotent replay (see Idempotency):

{
"data": {
"id": "suoPF5JPqgo2NvMmMfR6mg",
"orderId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"type": "transfer",
"status": "pending",
"vaultId": "kR7mNpX2wQvL9sYhBjD4eT",
"organizationId": "djk2wDuMhsx9KR2r7JgBQW",
"sourceAddressId": "gMP71sR5sNUnGdKFTsNzp6",
"destinationAddress": "0x742d35cc6634c0532925a3b844a0e9e2e8f8b8b8",
"network": "ethereum-mainnet",
"asset": "c60_t0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "1000.50",
"approvalString": "…",
"metadata": {},
"createdAt": "2026-07-15T12:00:00.000Z",
"updatedAt": "2026-07-15T12:00:00.000Z"
}
}

Initial status is pending when no quorum is required, or approval-pending when vault quorum approvals are needed.

Errors

Common 400 responses:

Message (substring)Cause
addressId is required for non-UTXO networksMissing source on an account-based network
For UTXO networks, one of addressId, accountId, or vaultId must be providedMissing UTXO source
changeAddressId is required when addressId is not providedUTXO send from accountId/vaultId without options.changeAddressId
Vault is not activeSource vault is not in active status
Address is not revealedTezos source address has no on-chain reveal yet
Minimum transfer amount for …amount is below the network/asset minimum
Source and destination addresses cannot be the sameSelf-transfer not allowed on this network
Destination address requires a memoDestination needs options.memo on memo-mandatory networks
Address account does not match / Address vault does not matchConflicting source fields

Idempotency

The orderId field is the idempotency key. Submitting the same orderId twice does not create a duplicate — the API returns the existing transaction object.

The HTTP status code distinguishes create vs replay; read it explicitly:

HTTPMeaning
201A new transaction was created for this orderId.
200A transaction with this orderId already exists; the same object is returned.

Replays return the existing transaction regardless of its current status, so data.status is the canonical signal for progress. Generate a fresh UUID per logical business action; on timeouts/network errors reuse the same orderId so retries don’t spawn duplicates.

orderId is unique per vault, not globally. Two different vaults may use the same orderId. Consequently:

  • Idempotency is scoped to the vault the source belongs to — the same orderId in a different vault creates a separate transaction.
  • A replacement transaction deliberately reuses its parent’s orderId (so one vault can hold several transactions with the same orderId).
  • When you fetch a transaction by orderId (below), pass vaultId to disambiguate — otherwise a cross-vault collision returns 409 TX-2002.

Get transaction details

# By transaction ID (default)
curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}

# By blockchain hash
curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/transactions/{hash}?idType=blockchainHash"

# By order ID (pass vaultId to disambiguate across vaults)
curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/transactions/{orderId}?idType=orderId&vaultId={vaultId}"

idType accepts id (default), blockchainHash, or orderId. The response includes vault, lifecycle events, and creator by default; account, address, approvals, decodedPayload, and quorum are added when the caller has the relevant permissions on the vault.

Ambiguous orderId

Because orderId is unique per vault, looking one up without vaultId returns 404 when nothing matches, the transaction when exactly one vault matches, and 409 TX-2002 when the same orderId exists in more than one accessible vault. Pass vaultId for a deterministic lookup. For a replacement chain in a single vault, the lookup returns the mined replacement if present, otherwise the original.

Decoded payload

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/decoded

Returns the transaction with its decoded payload — useful for verifying the exact operation that will be signed (calldata decoding, recipients, amounts) before approving.

Lifecycle events

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/events

Returns the persisted event timeline for the transaction. Event names mirror lifecycle statuses plus warning (not a status). Typical names: pending, approval-pending, approved, requested, to-sign, signed, submitted, mined, warning, to-cancel, failed, cancelled, replaced.

data shape depends on eventName:

eventNamedata
approved{ approvalsCount }
warning{ reasonCode, reasonDetails? }reasonDetails is populated for insufficient_balance (per address/asset shortfall breakdown)
to-cancel, cancelled{ reasonCode? }
others{}

rolledback is not stored here — it appears only as an outgoing webhook. Use this endpoint to reconstruct history post-hoc; for live updates rely on Webhooks.

List transactions

curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/transactions?organizationId=djk2wDuMhsx9KR2r7JgBQW"

organizationId is required. Vault scoping is automatic — without manageVaults on the organization (or Vault:read global), results are restricted to vaults the caller has read on.

Filters

All filters use bracket notation and accept arrays (repeat the parameter, or use comma-separated values per your HTTP client).

FilterNotes
filter[id]Internal transaction IDs.
filter[orderId]Your orderIds.
filter[hash]On-chain transaction hashes.
filter[vaultId]Vault IDs (intersected with the caller’s accessible vaults).
filter[accountId]Account IDs.
filter[network]Custody network codes.
filter[asset]Universal Asset IDs.
filter[sourceAddressId]Source address IDs.
filter[status]One or more of the lifecycle statuses (see Transaction Lifecycle).
filter[creatorId]Client IDs of transaction creators.
relations[]Hydrate optional relations: vault, account, address, events, approvals, creator.
searchFree-text search over address name / network address / hdpath (min 3 chars).
page, pageSizeDefaults page=0, pageSize=100. Max pageSize=1000.
# Status + network, hydrate vault and approvals
curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/transactions\
?organizationId=djk2wDuMhsx9KR2r7JgBQW\
&filter[status]=mined\
&filter[network]=ethereum-mainnet\
&relations[]=vault&relations[]=approvals"

The response is paginated with a total count — page through it using pagination.totalPages:

{
"data": [ /* transactions */ ],
"pagination": { "total": 1234, "page": 0, "pageSize": 100, "totalPages": 13 },
"empty": false
}

Blockchain-sourced transactions

List on-chain transactions involving your vault addresses (both outgoing and incoming/external):

curl -H "Authorization: Bearer $TOKEN" \
"https://api.carabaas.com/api/v1/transactions/blockchain\
?vaultId=kR7mNpX2wQvL9sYhBjD4eT&network=ethereum-mainnet"

One of vaultId / accountId / sourceAddressId is required; network is optional. Pagination uses page / pageSize and a hasMore cursor (no total).

Simulate a transaction

Preview fee/balance impact for a candidate transaction without persisting anything:

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"addressId": "gMP71sR5sNUnGdKFTsNzp6",
"destination": "0x742d35Cc...",
"network": "ethereum-mainnet",
"asset": "c60",
"amount": "1.0"
}' \
https://api.carabaas.com/api/v1/transactions/simulate

The body is a regular CreateTxDto. The endpoint runs the simulation for all three fee priorities (low, medium, high) and returns an array of results, each with the projected balanceChanges and chain-specific metadata (e.g. gasPrice, gasLimit on ETH-like):

{
"data": [
{
"feePriority": "low",
"balanceChanges": [ /* projected debits/credits */ ],
"metadata": { "gasPrice": "1000000000", "gasLimit": "21000" }
},
{ "feePriority": "medium", "balanceChanges": [ /* … */ ], "metadata": { /* … */ } },
{ "feePriority": "high", "balanceChanges": [ /* … */ ], "metadata": { /* … */ } }
]
}

Approve a transaction

When a transaction sits in approval-pending, approvers in the vault’s quorum sign it via:

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"keyId": "the-approver-key-id",
"approval": "<signature-over-approvalString>"
}' \
https://api.carabaas.com/api/v1/transactions/{txId}/approve

Constraints:

  • Caller must have approve permission on the vault.
  • Transaction must currently be in approval-pending; otherwise 400 Transaction is not in approval-pending status.
  • Transaction must have a quorum attached; otherwise 400 Transaction has no quorum associated.

The signed string is over the transaction’s approvalString (deterministic, returned in the transaction object). Once the quorum threshold is met the transaction transitions to approved and proceeds automatically. See Approval Workflow for the end-to-end signing protocol.

List approvals

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/approvals

Returns approvals collected so far, each with the approving key and client. Caller needs read on the vault (or manageVaults on the org); approver identity (hideApprovers) is hidden unless the caller also has approve on the vault or Vault:read globally.

Master approval payload

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/master-approval-payload

Returns { approvalString, approvals, encodedTransaction, payloads } — the bundle a master approver needs to issue the final signature. Applies to vaults with the master-approvals feature enabled. There are no dedicated master-approval webhook types — track progress via the standard outgoing stream (approved, to-sign, …) and poll this endpoint when needed. Caller must have approve on the vault, and the transaction must already have its payload constructed (otherwise 400 Transaction has no payload constructed yet).

External-signer signing

Vaults configured with an external signer are not signed by the platform. When such a transaction reaches to-sign, you fetch the prepared payloads, sign them yourself, and submit the signatures back. Vaults signed by the platform skip this entirely and move straight to signed.

Both signing endpoints require sign permission on the vault.

Wait for the to-sign webhook (or poll the transaction status), then:

1. Fetch the signing payloads

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/signing-payloads

Returns { encodedTransaction, payloads, network, approvalString, approvals, organizationId }. Each entry in payloads has data (the hash to sign), hdpath, publicKey (curve + hex), and keychainId — sign each data with the key for its curve.

2. Submit the signatures

curl -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"signatures": [
{ "payload": "0x…", "hdpath": "m/44/60/0/0/0", "signature": "0x…" }
]
}' \
https://api.carabaas.com/api/v1/transactions/{txId}/signatures

Provide one signature per payload, in payload order. Once accepted, the transaction advances to signed and the platform broadcasts it. Re-submitting the same signatures is idempotent (a no-op), so a retried PUT is safe.

Decline a transaction

curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}/decline

Requests cancellation of a transaction that has not yet been signed. The transaction transitions to to-cancel while the platform unwinds processing, then to cancelled (reasonCode: declined_by_client). There is no declined webhook — subscribe to cancelled for the terminal outcome (see Webhooks).

Caller must have decline permission on the vault. A decline is honored only while the transaction is still in pending, approval-pending, approved, requested, or to-sign. Once it is signed or submitted, decline no longer applies (the call is a no-op and returns the transaction unchanged) — use Replace for ETH-like networks, or wait for the network outcome.

Replace a transaction

Replace a still-unmined transaction with a higher-fee version (e.g. RBF / fee bump):

curl -X PATCH \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "feePriority": "high" }' \
https://api.carabaas.com/api/v1/transactions/{txId}/replace

Body fields (all optional):

FieldNotes
feePrioritylow | medium | high. Default for replacement is high.
gasPriceDecimal string. Overrides feePriority when set.
gasLimitDecimal string. Falls back to the parent transaction’s value if omitted.

Constraints (return 400 otherwise):

  • Only Ethereum-like networks support replacement.
  • The original transaction must be in submitted status.
  • A replacement transaction itself cannot be replaced — bump the parent instead.

Caller must have initiate on the vault. The replacement carries a new transaction id but reuses the parent’s orderId; on success the parent eventually transitions to replaced (terminal) once the replacement mines.

Warnings and retries

When the platform pauses processing for a transient reason it emits a warning event (in addition to the lifecycle status, which stays unchanged) and retries automatically. Reason codes you’ll see on the warning payload:

Reason codeWhat it means
insufficient_balanceSource address can’t cover amount + fee at signing time. Top up the address — the platform picks the transaction back up.
hosted_singner_unavailableA signer is temporarily unavailable (our side). Resolves automatically.

Webhooks carry reasonCode only. For insufficient_balance, structured reasonDetails (per address/asset shortfall) is available on the warning entry from GET .../events — use it to show how much to top up.

If the underlying condition resolves, processing continues from where it stopped — no new transaction is created, the same id/orderId proceeds. If it doesn’t resolve within the platform’s retry window, the transaction is cancelled with a matching reasonCode. Subscribe to webhooks (warning, cancelled) to surface this to your users.

Special transaction types

The base POST /api/v1/transactions flow handles standard transfers. The following endpoints create specialised custody transactions that share the same lifecycle, idempotency, and approval semantics:

Trustline (Stellar / Ripple)

POST /api/v1/transactions/trustline — open an asset trustline so the address can hold a non-native token. See Stellar & Ripple — Trustlines.

Reveal (Tezos)

POST /api/v1/transactions/reveal — one-time on-chain public-key reveal required before any outgoing Tezos operation. See Tezos Reveal.

ERC-20 / TRC-20 transfer-from

POST /api/v1/transactions/transfer-from — pull tokens previously approved to a spender address.

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"addressId": "8uPDmg3KsbUfsB8dx",
"spenderAddressId": "mnfUjfKLCxFbLZQmB",
"destination": "0xRecipient...",
"network": "ethereum-mainnet",
"asset": "c60_t0x779877A7B0D9E8603169DdbD7836e478b4624789",
"amount": "150",
"feePriority": "medium"
}' \
https://api.carabaas.com/api/v1/transactions/transfer-from

Both addressId (the token holder / from) and spenderAddressId (the address paying gas in the native coin) must belong to the same vault.

Setup-allowance flow

POST /api/v1/transactions/flows/setup-allowance — multi-step orchestrated flow that configures an ERC-20/TRC-20 allowance from ownerAddressId for spenderAddressId.

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ownerAddressId": "8uPDmg3KsbUfsB8dx",
"spenderAddressId": "mnfUjfKLCxFbLZQmB",
"network": "ethereum-mainnet",
"asset": "c60_t0x779877A7B0D9E8603169DdbD7836e478b4624789",
"feePriority": "medium"
}' \
https://api.carabaas.com/api/v1/transactions/flows/setup-allowance

orderId (UUID) is optional — if provided it acts as the flow-level idempotency key. The response is a Flow object (not a Transaction); the underlying erc20_approve / trc20_approve transactions are created inside the flow. Key fields: id, type, status, vaultId, ownerAddressId, spenderAddressId, network, asset, feePriority, and optional feeTransactionOrderId / approvalTransactionOrderId linking the child transactions.

Manage flows via:

  • GET /api/v1/transactions/flows/{id} — fetch by id (default) or orderId (?idType=orderId).
  • PATCH /api/v1/transactions/flows/{id}/decline — request flow cancellation.

See Also