Skip to main content

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.
Automatic execution

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.

FieldTypeDescription
permissions.operationRequesterbooleanMakes the access a requester: its sensitive operations go through approval. Defaults to false.
permissions.operationApproverbooleanMakes the access an approver. Defaults to false.
approverAccessIdsarray of stringFor a requester: the access ids of its approvers. They must belong to the same Business Account.
minApprovalsintegerFor a requester: how many of its approvers must approve. Required with approverAccessIds, between 1 and their count.
multisigNotificationsbooleanWhether 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 typeRequest
CREATE_TICKETPOST /v2/account/tickets/, for a pay-out or subaccount transfer without conversion (see Tickets)
CREATE_BENEFICIARY_WALLETPOST /v2/account/beneficiaries/wallets/
QUALIFY_BENEFICIARY_WALLETPATCH /v2/account/beneficiaries/wallets/{wallet-id}/
CREATE_BENEFICIARY_<RAIL>_BANK_ACCOUNTPOST /v2/account/beneficiaries/bank-accounts/<rail>/, for brl, usd, eur, ars, cop, mxn and swift
FEE_ACCOUNT_WITHDRAWPOST /v2/account/fee-account/withdraw
FEE_ACCOUNT_DEPOSITPOST /v2/account/fee-account/deposit

Tickets​

Whether a ticket needs approval depends on where its money comes from and whether it converts currencies:

TicketExampleFor a requester
Deposit: not paid from the account balancePix, wire or on-chain depositRuns immediately, no approval
Conversion: currencies change and the funds stay in the accountBRLA → USDC in the accountRuns immediately, no approval
Pay-out without conversion: paid from the balance, same currencyBRLA balance → Pix, USDC balance → walletHeld for approval (CREATE_TICKET)
Transfer without conversion between the account and its subaccountsBRLA from the main account to a subaccountHeld for approval (CREATE_TICKET)
Pay-out with conversion: paid from the balance in another currencyBRLA balance → USD wireProhibited: 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​

StatusMeaning
PENDINGWaiting for approvals.
APPROVEDThe quorum was reached but the operation hasn't run yet (see When it can't run).
EXECUTEDThe operation was executed. executedId holds the id of what it created (for example the ticket or the beneficiary).
CANCELEDAn 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 EXECUTED without an executedId, 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 to APPROVED and can be executed again, and their idempotency key guarantees the funds move only once. If the request itself is refused, they also end EXECUTED.

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
}
}
FieldTypeDescription
operationIdstringThe operation.
approvalsintegerApprovals so far.
executionobjectPresent only on the approval that reached the quorum.
execution.statusstringEXECUTED or FAILED.
execution.resultobjectWith EXECUTED: the response of the executed request.
execution.errorstringWith FAILED: why the operation didn't run.
execution.errorCodeintegerWith 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​

FieldTypeRequiredDescription
reasonstringNoWhy 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​

FieldTypeDescription
statusstringPENDING, APPROVED, EXECUTED or CANCELED.
operationTypestringOne of the operation types.
requesterEmailstringApprover list only: operations of this requester.
cursorstringThe 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​

StatusErrorWhen
403payout operations with currency conversion are not available for multi-sig requester usersA requester created a pay-out with conversion (see Tickets).
403self approval is not permittedA requester tried to approve or reject its own operation.
403you are not an approver for this requesterThe access isn't one of the requester's approvers.
404operationThe operation doesn't exist.
409operation is not pendingApproving an operation that is no longer PENDING.
409you have already approved this operationThe same approver approved twice.
409operation is not approved or is already executedExecuting an operation that isn't APPROVED.
409operation was canceled and can no longer be executedExecuting a rejected operation.