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:
type | Algorithm | Encoding | Notes |
|---|---|---|---|
rsa | RSA-SHA256, PKCS#1 v1.5 padding (RS256) | hex | Recommended for most automation. Min recommended modulus: 2048 bits. |
address | secp256k1 / Ethereum personal_sign | 0x… 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_PADDINGis what the platform expects. Do not useRSA_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:
| Field | Type | Description |
|---|---|---|
keyId | string | ID of an active approver key belonging to the calling client. |
approval | string | Hex 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
| Status | Body | When |
|---|---|---|
| 400 | Transaction is not in approval-pending status | Order already approved/declined/cancelled, or not yet pending. |
| 400 | Transaction has no quorum associated | Vault did not have an active quorum at creation time. |
| 400 | Quorum is not active | Vault quorum has been rotated or suspended. |
| 400 | Client or key is not found or not active | Caller or keyId is not part of the active quorum. |
| 400 | Invalid approval signature: … | Signature does not verify against approvalString + key. |
| 403 | Forbidden | Caller 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-pendingentirely and go straight frompendingtoapproved. - An approval submitted while in
pendingreturns400 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 theapproval-pendingwebhook.
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].