Skip to main content

Approval Workflow

Each vault has a configurable quorum (M-of-N): M required approvals from N eligible approvers. Every approval is a cryptographic signature over a deterministic payload — this gives you authenticity (only the approver's key could have signed), non-repudiation (the signature is recorded against the approver's client identity), and integrity (any change to the order invalidates every collected signature).

This page covers the transaction approval flow. The same primitives drive address-book approvals (see Address Books) and quorum-change approvals (see Vaults — Quorum Management).

How it works

When a vault has an active quorum, every newly created transaction enters approval-pending. Each approver signs the transaction's approvalString with the private half of one of their registered approver keys and submits the signature. Once the M-of-N threshold is met the platform transitions the transaction to approved, builds the blockchain payload, generates a master approval that binds the approved order to the exact on-chain transaction, and forwards both to the cosigners. Each cosigner independently re-verifies all collected approvals before contributing its MPC partial signature.

What gets signed

The signed string is the transaction's approvalString — a canonical (RFC-8785-style) JSON string deterministically derived from the order parameters. It is generated server-side at order creation, stored on the transaction, and never changes for the lifetime of the order.

Approvers must sign the exact bytes returned in transaction.approvalString. Do not re-serialize the order yourself — keys are sorted and undefined/empty fields are omitted, so any difference will produce a signature the platform rejects.

Fetch it from any transaction read endpoint, e.g.:

curl -H "Authorization: Bearer $TOKEN" \
https://api.carabaas.com/api/v1/transactions/{txId}
# response.data.approvalString — UTF-8 string, sign these exact bytes

A typical approvalString looks like (JSON formatted for readability):

{
"addressId": "3RkawT1Ey9iNpDSK",
"amount": "1000000000000000000",
"asset": "c1",
"destination": "0xab…",
"network": "ethereum-mainnet",
"options": {},
"orderId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}

If you need the on-chain payload alongside the approvalString (e.g. to display decoded transaction data to a human approver), use the master-approval-payload endpoint described below.

Approver keys

An approver authenticates to the platform with a JWT (their client identity) and proves intent with a separate approver key registered against their client. Two key types are supported:

typeAlgorithmEncodingNotes
rsaRSA-SHA256, PKCS#1 v1.5 padding (RS256)hexRecommended for most automation. Min recommended modulus: 2048 bits.
addresssecp256k1 / Ethereum personal_sign0x… hex (EIP-191)Useful when your approver is a hardware wallet or EVM-style signer.

Keys are managed via the organization auth-key endpoints (see Authentication). Only active keys belonging to an active client of the vault's quorum can produce a valid approval — blocked or revoked keys are rejected with 400 Key not found.

Approve via API

Step 1 — Read the approvalString

Already returned on every transaction read; the most direct way is:

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

Take response.data.approvalString exactly as returned.

Step 2 — Sign the approvalString

RSA key (type: rsa)

const crypto = require('crypto');
const fs = require('fs');

const privateKey = fs.readFileSync('approver_private_key.pem');
const approvalString = transaction.approvalString; // from Step 1

// RSA-SHA256, PKCS#1 v1.5 padding, hex-encoded
const approval = crypto
.createSign('RSA-SHA256')
.update(approvalString, 'utf8')
.sign(privateKey, 'hex');

Default RSA_PKCS1_PADDING is what the platform expects. Do not use RSA_PKCS1_PSS_PADDING — those signatures will be rejected.

Ethereum-style key (type: address)

const { Wallet } = require('ethers');

const wallet = new Wallet(privateKey);
const approval = await wallet.signMessage(approvalString); // 0x… hex

ethers.signMessage applies the \x19Ethereum Signed Message:\n… prefix; the platform recovers the signer with the matching algorithm and compares it to the address registered on the key.

Step 3 — Submit the approval

curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"keyId": "8uPDmg3KsbUfsB8dx",
"approval": "<hex-signature-from-step-2>"
}' \
https://api.carabaas.com/api/v1/transactions/{txId}/approve

Request body:

FieldTypeDescription
keyIdstringID of an active approver key belonging to the calling client.
approvalstringHex signature of approvalString produced with the private half of the key.

Response: the updated TransactionDto. The new approval also appears in GET /transactions/{txId}/approvals.

Errors you may see

StatusBodyWhen
400Transaction is not in approval-pending statusOrder already approved/declined/cancelled, or not yet pending.
400Transaction has no quorum associatedVault did not have an active quorum at creation time.
400Quorum is not activeVault quorum has been rotated or suspended.
400Client or key is not found or not activeCaller or keyId is not part of the active quorum.
400Invalid approval signature: …Signature does not verify against approvalString + key.
403ForbiddenCaller lacks approve on the vault.

Idempotency and resubmission

Approvals are unique per (transactionId, clientId). Re-submitting a valid signature for the same approver is a no-op — the existing record stays and the response still returns 200/201 with the current transaction state. Different approvers contribute independently and may submit in any order.

To change your mind before the threshold is reached, decline the transaction (see below). There is no "revoke approval" endpoint for transactions — once the quorum is met the transaction proceeds.

Inspect the master-approval payload

For audit, decoding, or pre-signature display, use:

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

Returns:

{
"approvalString": "{\"addressId\":\"…\",\"amount\":\"…\", … }",
"approvals": [ /* approvals collected so far, with key + client */ ],
"encodedTransaction": "0x… (network-native serialised transaction)",
"payloads": [ /* sighashes that will be MPC-signed */ ]
}

encodedTransaction and payloads are populated only after the quorum is met and the platform has built the blockchain transaction. Caller must hold approve on the vault.

Decline a transaction

Any approver (or anyone with decline on the vault) can cancel a transaction that is still cancellable (pending, approval-pending, approved, requested, to-cancel):

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

The transaction transitions to cancelled with reasonCode: declined_by_client. Other reason codes you may observe on cancelled transactions: rejected_by_approver, rejected_by_master_approver, order_expired, transaction_expired, insufficient_balance, not_enough_utxos, singner_unavailable, invalid_order, replacement_status_invalid — see Transactions.

List approvals

View who has signed and how close the transaction is to its quorum:

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

Each item contains id, keyId, clientId, type, approval, approvalData, createdAt, plus the embedded key and client. Approver identity (clientId, client) is hidden unless the caller has approve on the vault or the global Vault:read permission.

Status flow

pending → approval-pending → approved → requested → signed → submitted → mined
↓ ↓ ↓
cancelled cancelled cancelled / failed / replaced
  • Vaults without a quorum skip approval-pending entirely and go straight from pending to approved.
  • An approval submitted while in pending returns 400 Transaction is not in approval-pending status — wait for the platform to admit the order to the queue (typically the next event loop tick) or rely on the approval-pending webhook.

See Transactions — Lifecycle for the full state machine and webhook event names.

Quorum configuration

Quorum settings (groups, approvers, M-of-N consensus) live on the vault. Setup, rotation, and join flow are described in Vaults — Quorum Management.

A note on terminology: the vault stores groups[].consensus (M, the number of required signatures) and groups[].approvers (the eligible client IDs, total = N). The formula field returned on QuorumDto exposes this as a tuple [M, N].

See also