Multi-Signature Operations
Multi-signature (multi-sig) makes an access's sensitive operations wait for approval from other accesses before they run. Instead of executing a request immediately, Avenia stores it as a pending operation and runs it only once enough of the assigned approvers have approved it.
It is configured per access, with two roles:
- Requester: an access whose sensitive operations require approval. Each requester has its own list of approvers and a minimum number of approvals (the quorum).
- Approver: an access that can approve or reject the operations of the requesters it is assigned to.
When an approval makes the operation reach its quorum, the operation is executed automatically, on behalf of the requester, as part of that approval. The requester doesn't need to call Execute. See Automatic execution.
Setting up requesters and approvers
Roles are set with the permissions object of the Accesses Management API, by an admin access with MFA enabled.
| Field | Type | Description |
|---|---|---|
permissions.operationRequester | boolean | Makes the access a requester: its sensitive operations go through approval. Defaults to false. |
permissions.operationApprover | boolean | Makes the access an approver. Defaults to false. |
approverAccessIds | array of string | For a requester: the access ids of its approvers. They must belong to the same Business Account. |
minApprovals | integer | For a requester: how many of its approvers must approve. Required with approverAccessIds, between 1 and their count. |
multisigNotifications | boolean | Whether the access receives multi-sig emails (see Notifications). |
A requester can't approve or reject its own operations.
Which operations need approval
For a requester, these requests are held for approval instead of running:
| Operation type | Request |
|---|---|
CREATE_TICKET | POST /v2/account/tickets/, for a pay-out or subaccount transfer without conversion (see Tickets) |
CREATE_BENEFICIARY_WALLET | POST /v2/account/beneficiaries/wallets/ |
QUALIFY_BENEFICIARY_WALLET | PATCH /v2/account/beneficiaries/wallets/{wallet-id}/ |
CREATE_BENEFICIARY_<RAIL>_BANK_ACCOUNT | POST /v2/account/beneficiaries/bank-accounts/<rail>/, for brl, usd, eur, ars, cop, mxn and swift |
FEE_ACCOUNT_WITHDRAW | POST /v2/account/fee-account/withdraw |
FEE_ACCOUNT_DEPOSIT | POST /v2/account/fee-account/deposit |
Tickets
Whether a ticket needs approval depends on where its money comes from and whether it converts currencies:
| Ticket | Example | For a requester |
|---|---|---|
| Deposit: not paid from the account balance | Pix, wire or on-chain deposit | Runs immediately, no approval |
| Conversion: currencies change and the funds stay in the account | BRLA → USDC in the account | Runs immediately, no approval |
| Pay-out without conversion: paid from the balance, same currency | BRLA balance → Pix, USDC balance → wallet | Held for approval (CREATE_TICKET) |
| Transfer without conversion between the account and its subaccounts | BRLA from the main account to a subaccount | Held for approval (CREATE_TICKET) |
| Pay-out with conversion: paid from the balance in another currency | BRLA balance → USD wire | Prohibited: returns 403 Forbidden |
To pay out in another currency, a requester first converts inside the account (no approval needed), then creates the pay-out in the new currency, which goes through approval.
A prohibited ticket is refused right away and no operation is created:
{
"error": "ForbiddenError",
"extraInfo": "payout operations with currency conversion are not available for multi-sig requester users"
}
Response to a held request
A held request returns 202 Accepted with the id of the pending operation, instead of the usual response:
{
"operationId": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"status": "PENDING"
}
The operation keeps the request exactly as it was sent, including the subAccountId it was made for: when it runs, it acts on that subaccount, in the requester's name.
Lifecycle
| Status | Meaning |
|---|---|
PENDING | Waiting for approvals. |
APPROVED | The quorum was reached but the operation hasn't run yet (see When it can't run). |
EXECUTED | The operation was executed. executedId holds the id of what it created (for example the ticket or the beneficiary). |
CANCELED | An approver rejected the operation. It can no longer be approved or executed. |
Automatic execution
The approval that makes an operation reach its minApprovals executes it right away, on behalf of the requester:
- It runs with the requester's account, permissions and subaccount, not the approver's. The approver only provides the last signature.
- Before running, Avenia checks the requester again, as if they had executed it themselves: the access must still exist, still be a requester, not be view-only, and still be allowed to act on the operation's account or subaccount.
- It happens once: if two approvers approve at the same moment, only the approval that reaches the quorum executes the operation.
- The approval is kept even if the execution fails.
The response to that approval carries the outcome in an execution field. Approvals that don't reach the quorum have no execution field.
When it can't run
If the operation can't run when the quorum is reached, the approval still returns 200 with execution.status: "FAILED" and the reason. Two cases:
- Refused before running (for example, the requester lost its permission, or the subaccount is no longer available): the operation stays
APPROVED. Once the cause is fixed, the requester can run it with Execute. - Failed while running (for example, the quote can no longer be honored for a ticket): the operation ends
EXECUTEDwithout anexecutedId, and the request has to be made again. Fee account withdrawals and fee account deposits (funds moved between the main account and its fee account, not a deposit ticket) are the exception when the failure is temporary, such as an internal error or a timeout: they return toAPPROVEDand can be executed again, and their idempotency key guarantees the funds move only once. If the request itself is refused, they also endEXECUTED.
Approve Operation
Approves a pending operation. Only an approver assigned to the operation's requester can approve it, once.
HTTP POST Request
POST https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/{operation-id}/approve
cURL Example
curl -X POST "https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90/approve" \
-H "Authorization: Bearer <token>"
JSON Response
Quorum not reached yet:
{
"operationId": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"approvals": 1
}
Quorum reached, operation executed:
{
"operationId": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"approvals": 2,
"execution": {
"status": "EXECUTED",
"result": {
"id": "1f6a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8"
}
}
}
execution.result is the response the original request would have returned (for a ticket, the ticket's id).
Quorum reached, operation could not run:
{
"operationId": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"approvals": 2,
"execution": {
"status": "FAILED",
"error": "ForbiddenError: the requester can no longer execute this operation",
"errorCode": 403
}
}
| Field | Type | Description |
|---|---|---|
operationId | string | The operation. |
approvals | integer | Approvals so far. |
execution | object | Present only on the approval that reached the quorum. |
execution.status | string | EXECUTED or FAILED. |
execution.result | object | With EXECUTED: the response of the executed request. |
execution.error | string | With FAILED: why the operation didn't run. |
execution.errorCode | integer | With FAILED: the HTTP status the execution ended with. |
Reject Operation
Rejects an operation that is PENDING or APPROVED, which cancels it for good. Only an approver assigned to the operation's requester can reject it.
HTTP POST Request
POST https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/{operation-id}/reject
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Why it was rejected, up to 500 characters. Shown to the requester. |
cURL Example
curl -X POST "https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90/reject" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "reason": "Wrong beneficiary" }'
JSON Response
{
"operationId": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"status": "CANCELED"
}
Execute Operation
Runs an operation that is APPROVED. With automatic execution, this is only needed when the operation couldn't run at the moment its quorum was reached (see When it can't run). Only the operation's requester can call it.
HTTP POST Request
POST https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/{operation-id}/execute
cURL Example
curl -X POST "https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90/execute" \
-H "Authorization: Bearer <token>"
JSON Response
201 Created with the response the original request would have returned (for a ticket, the ticket's id).
List Operations
Two lists: the operations waiting on you as an approver, and the operations you requested.
HTTP GET Request
GET https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/approver
GET https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/requester
Query Parameters
| Field | Type | Description |
|---|---|---|
status | string | PENDING, APPROVED, EXECUTED or CANCELED. |
operationType | string | One of the operation types. |
requesterEmail | string | Approver list only: operations of this requester. |
cursor | string | The cursor of the previous page, to read the next one. |
cURL Example
curl -X GET "https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/approver?status=PENDING" \
-H "Authorization: Bearer <token>"
JSON Response
{
"operations": [
{
"id": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"requesterEmail": "requester@company.com",
"operationType": "CREATE_BENEFICIARY_WALLET",
"payload": { "walletAddress": "0xXXXX", "chain": "POLYGON" },
"status": "PENDING",
"approvals": 1,
"minApprovals": 2,
"createdAt": "2026-10-27T14:37:02Z",
"subAccountId": "c0a8012e-7b1d-4f3a-9e2c-5d6f7a8b9c0d",
"subAccountName": "Store"
}
],
"cursor": ""
}
An empty cursor means there are no more pages.
Get Operation
Returns one operation of your Business Account, with each approver's decision.
HTTP GET Request
GET https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/{operation-id}
cURL Example
curl -X GET "https://api.sandbox.avenia.io:10952/v2/account/multi-sig-operations/9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90" \
-H "Authorization: Bearer <token>"
JSON Response
{
"id": "9b2c6f5e-3a41-4c1e-9d0a-6f1e2b7c8d90",
"requesterEmail": "requester@company.com",
"operationType": "CREATE_BENEFICIARY_WALLET",
"payload": { "walletAddress": "0xXXXX", "chain": "POLYGON" },
"status": "EXECUTED",
"approvals": 2,
"minApprovals": 2,
"approvers": [
{
"accessId": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"email": "approver1@company.com",
"hasApproved": true
},
{
"accessId": "6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0",
"email": "approver2@company.com",
"hasApproved": true
}
],
"createdAt": "2026-10-27T14:37:02Z",
"executedId": "1f6a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8"
}
A rejected operation also carries rejectedByEmail, rejectionReason and rejectedAt.
Notifications
With multisigNotifications enabled:
- Approvers are emailed when one of their requesters creates an operation.
- Requesters are emailed when one of their operations is approved, or is rejected.
Turning it off only stops the emails.
Common Errors
| Status | Error | When |
|---|---|---|
| 403 | payout operations with currency conversion are not available for multi-sig requester users | A requester created a pay-out with conversion (see Tickets). |
| 403 | self approval is not permitted | A requester tried to approve or reject its own operation. |
| 403 | you are not an approver for this requester | The access isn't one of the requester's approvers. |
| 404 | operation | The operation doesn't exist. |
| 409 | operation is not pending | Approving an operation that is no longer PENDING. |
| 409 | you have already approved this operation | The same approver approved twice. |
| 409 | operation is not approved or is already executed | Executing an operation that isn't APPROVED. |
| 409 | operation was canceled and can no longer be executed | Executing a rejected operation. |