Transactions
Transaction Lifecycle
Every transaction follows a defined state machine from creation to on-chain confirmation (or terminal failure):
| Status | Description | Next |
|---|---|---|
pending | Created; no quorum required (or quorum already satisfied at creation time). | requested, to-cancel, failed |
approval-pending | Waiting for required vault-quorum approvals. | approved, to-cancel, cancelled |
approved | Quorum reached; waiting to be sent to signing. | requested, to-cancel |
requested | Queued for signing. | to-sign, signed, to-cancel, failed |
to-sign | External-signer vaults only — payloads prepared, awaiting your signatures (see External-signer signing). | signed, to-cancel |
signed | Signature complete. | submitted, failed |
submitted | Broadcast to the blockchain. | mined, replaced, failed |
mined | Confirmed on chain (terminal — happy path). | — |
to-cancel | Cancellation requested via Decline; platform is unwinding. | cancelled |
cancelled | Cancelled (terminal — see reasonCode on the event for details). | — |
failed | Failed on submission/processing (terminal). | — |
replaced | Superseded 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
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
| Field | Required | Description |
|---|---|---|
orderId | Yes | A valid UUID string. Idempotency key — see Idempotency. |
addressId | Conditional | Source address. Required for non-UTXO networks. For UTXO networks one of addressId / accountId / vaultId must be provided. |
accountId | Conditional | Source account (UTXO chains only) — collect UTXOs across all addresses in the account. |
vaultId | Conditional | Source vault (UTXO chains only) — collect UTXOs across all addresses in the vault. |
destination | Yes | Recipient on-chain address. Alphanumeric string, 12–200 characters ([a-z0-9], case-insensitive). Canonicalized to the network’s wire format when applicable. |
network | Yes | Custody network identifier (e.g. ethereum-mainnet, bitcoin-mainnet). |
asset | Yes | Universal Asset ID for that network. |
amount | Yes | Decimal string in the asset’s human units (e.g. "1000.50"). |
feePriority | No | One of low | medium | high. Default medium. (custom is not accepted — use options.gasPrice on ETH-like networks instead.) |
note | No | Up to 256 chars. Visible only to you — saved with the transaction record. |
reference | No | Up to 128 chars. Shared field — visible to you and to the counterparty integrations. |
options | No | Network-specific knobs — see below. |
options
| Option | Applies to | Description |
|---|---|---|
gasPrice | ETH-like | Decimal string. Takes precedence over feePriority when set. |
gasLimit | ETH-like | Decimal string. |
data | ETH-like | Hex-encoded calldata (0x…) for smart-contract interaction. |
memo | XRP, ATOM, EOS, HBAR, LUNA/LUNC, XDB, XEM, XLM, ALGO | Destination tag / memo. Wire format depends on the chain. |
changeAddressId | UTXO | Address that should receive the change output. Must belong to the source vault. |
feePayerAddressId | Solana | Address paying the network fee. Must belong to the source vault. |
Source selection
| Network family | Required 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:
readon the organization that owns the vaultinitiateon 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 networks | Missing source on an account-based network |
For UTXO networks, one of addressId, accountId, or vaultId must be provided | Missing UTXO source |
changeAddressId is required when addressId is not provided | UTXO send from accountId/vaultId without options.changeAddressId |
Vault is not active | Source vault is not in active status |
Address is not revealed | Tezos 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 same | Self-transfer not allowed on this network |
Destination address requires a memo | Destination needs options.memo on memo-mandatory networks |
Address account does not match / Address vault does not match | Conflicting 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:
| HTTP | Meaning |
|---|---|
| 201 | A new transaction was created for this orderId. |
| 200 | A 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
orderIdin 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 sameorderId). - When you fetch a transaction by
orderId(below), passvaultIdto disambiguate — otherwise a cross-vault collision returns 409TX-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.
orderIdBecause 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:
eventName | data |
|---|---|
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).
| Filter | Notes |
|---|---|
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. |
search | Free-text search over address name / network address / hdpath (min 3 chars). |
page, pageSize | Defaults 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
approvepermission on the vault. - Transaction must currently be in
approval-pending; otherwise 400Transaction 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):
| Field | Notes |
|---|---|
feePriority | low | medium | high. Default for replacement is high. |
gasPrice | Decimal string. Overrides feePriority when set. |
gasLimit | Decimal 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
submittedstatus. - 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 code | What it means |
|---|---|
insufficient_balance | Source address can’t cover amount + fee at signing time. Top up the address — the platform picks the transaction back up. |
hosted_singner_unavailable | A 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 byid(default) ororderId(?idType=orderId).PATCH /api/v1/transactions/flows/{id}/decline— request flow cancellation.