require_approval rule into an approval workflow: people your app assigns approve the request in ordered stages before Para signs it. This page defines the approvalRequirement object, its limits, the approval settings your app can adjust without a new policy version, and the lifecycle of the approval case Para opens for each request.
Para turns on approval requirements per API key, so beta and production keys are enabled separately. Until it does, validating or saving a policy with an
approvalRequirement fails with permissions_v2_extension_unavailable; contact Para to enable them. Build and test requirements on a beta API key, shown as Development in the Developer Portal, then select Request production review to have Para apply the policy to your production key.Example
This Guardrail requires two finance approvals for native transfers of 1 ETH or more from app-owned wallets on Base. Transfers of 10 ETH or more also need one executive approval after finance. Treasury admins can change the finance count between 1 and 3 and move the executive threshold between 5 and 20 ETH.treasury-approvals.json
Requirement fields
AnapprovalRequirement is allowed only on a rule whose effect is require_approval. In a Request, every require_approval rule must have one.
Names, IDs and attribute values are trimmed, and each must be 1 to 64 characters.
Stages
Each stage needs a number of approvals from the people its eligibility selects. The next stage opens only after the current one reaches its count.
Each person counts once per case. Someone who approved in one stage is left out of later stages, so a two-stage requirement with two approvals each needs four different people.
Eligibility
A person is eligible for a stage when one of their assignments matches any clause in the stage’seligibility.
Names and values must already be in Unicode NFKC form; validation rejects other forms.
Your app assigns roles and attributes to users inside an authorization scope, a group name your app chooses, such as one per customer organization. Each wallet that a requirement governs must be bound to an authorization scope, and a stage selects only people assigned in that wallet’s scope. For a wallet without a binding, Para opens no case and the signing request fails with
403 FORBIDDEN and the message wallet has no trusted authorization scope binding. Use replaceUserApprovalAssignments and setWalletAuthorizationScope, listed in the REST SDK Permissions reference.
An assignment must use a role name or attribute value that an active policy declares, in a stage, in
configurationManagerEligibility or in an eligible_group option. Only users linked to your app, through a wallet connected to it, can receive assignments. For any other user, the call fails with user is not linked to this app.
Amount bands
Amount bands add stages as the amount grows. SetamountFact to the amount to read, and give each stage after the first a minimumAmount.
- The first stage has no
minimumAmountand applies to every amount the rule’swhenmatches. In a Guardrail, the amount at which approval starts is therefore fixed, because Guardrail parameters keep their defaults. - Every later stage needs a
minimumAmount. The thresholds must be positive, increasing integer strings without leading zeros. amountFactmust be available for every action the rule names.request.value.baseUnitssuitstransfer_native, andrequest.token.value.baseUnitssuitstransfer_tokens. See Facts and conditions.- For Safe payments on
execute_safe_transactionandsign_typed_data, userequest.safe.value.baseUnitsfor native payments orrequest.safe.token.value.baseUnitsfor ERC-20 payments. Each is present only for its own payment family, so pair it with a rule whosewhenreads the same family.
minimumAmount it reaches, in order, so in the example a 12 ETH transfer needs finance and then executive approval. If Para cannot read the amount when the request arrives, it does not open a case, and the request fails with approval_amount_unavailable.
Evidence
evidence sets what counts as an approval.
In a Request, a rule with
authenticated_approval evidence takes effect only after the user consents to the policy, like an allow rule. If the user chose to review each request, the user reviews first, then the case opens.
Requesters
The requester is the person who starts a request. Withauthenticated_approval evidence, the requester cannot approve their own request. For a user-owned wallet, Para knows the requester: the signed-in user who asked to sign. For an app-owned wallet, the request comes from your app, so pass the ID of the Para user who asked for it as initiatorUserId. The REST signing endpoints and the client SDK signMessage and signTransaction methods for app-owned wallets accept it. The user must be linked to your app, or the request fails with user is not linked to this app. If you omit initiatorUserId, Para has no person to exclude, so any eligible person, including the user who triggered the request, can approve or reject it.
When a request names initiatorUserId, Para applies these rules to that requester:
- Para leaves the user out of every stage, whatever the evidence.
- The user cannot approve or reject the case. A decision fails with
INITIATOR_CANNOT_APPROVEorINITIATOR_CANNOT_REJECT, and reading the case as an approver fails withINITIATOR_CANNOT_REVIEW. - The user can cancel the pending case. See Cancelling a case.
- Every later attempt of the same request must name the same
initiatorUserId.
Adjustable approval settings
safeKnobs declares settings that your app, or a configuration manager, can change after the policy is active without publishing a new version. Each setting targets one stage. A stage can have one participation setting, required_approvals or eligible_group, plus one minimum_amount setting.
Values are saved for one authorization scope, policy version and requirement, as an object of setting
id to value, with at most 25 entries. For the example:
- Your server secret can always write values. A user can write them when one of their assignments in that scope matches
configurationManagerEligibility. - An unknown setting
idfails withsafe_knob_not_declared, and a value of the wrong type or outside its range fails withsafe_knob_value_invalid. - Para applies the values and checks the requirement again, so thresholds must stay positive and increasing. A
minimum_amountvalue of"0", or one that is not above the previous stage’s threshold, fails withrequirement_schema_invalid. - Changing the values ends open cases for that scope and requirement. See Changes that end open cases.
Limits
These bounds cover requirement structure, saved settings, approver assignments and case lifetime.
If the first stage resolves to no eligible people, to fewer than it needs, or to more than 100, Para opens no case and the signing request fails with
400. When a later stage would resolve to more than 100 eligible people, the approval that would open it fails with 400 and the case stays pending. When it would resolve to fewer than it needs, the case ends as invalidated with stage_snapshot_invalid. Permissions errors lists the responses.
Cases
When arequire_approval rule with a requirement decides a request, Para opens an approval case and does not sign until the case completes. A REST signing call returns 403 POLICY_REVIEW_REQUIRED with the approvalCaseId. SDK signing with an app-owned wallet throws the same error, and the server SDK and React Native SDK carry the approvalCaseId in error.data.
The web and React SDKs currently reject with a generic error that has no data.
SDK signing with a user’s wallet holds the request as a pending transaction. See Review required.
Each stage takes a snapshot of its eligible people when it opens, so assignments added later do not join a stage that is already open.
For a readable Safe payment, the case’s request summary includes
safePayment: the payment kind, native or erc20, the Safe, the recipient, the amount in base units and the asset, plus from for an ERC-20 transferFrom.
Why a case ends
Why a case ends
Why a person cannot decide
Why a person cannot decide
A case read by a signed-in user includes
decision. When canDecide is false, reason is one of:Cancelling a case
Your server cancels a case withcancelPartnerApprovalCase in the REST SDK, or POST /v1/permissions-v2/approval-cases/{caseId}/cancel. The requester, including a user named in initiatorUserId, cancels it with cancelApprovalCase in the client SDKs.
Only a pending case can be cancelled. The case ends with status cancelled and terminalReason case_cancelled. When a pending transaction from a user’s wallet is waiting on the case, Para discards it, so that request never signs. A request that should still go ahead needs a new attempt, which opens a new case.
Cancelling a case that is no longer pending fails with 409 CONFLICT.
Changes that end open cases
Para checks these changes against open cases before it applies them:- Replacing, activating or deactivating a policy.
- Changing a user’s approval assignments.
- Binding a wallet to another authorization scope.
- Changing the adjustable approval settings.
An approval case is open while it is
pending, and after it completes until it is used to sign or expires. Para refuses a change that would invalidate open cases until you confirm it. A confirmed change ends those cases as invalidated, and their requests need a new attempt.Through the API, a refused change returns 409 with code APPROVAL_CASES_AFFECTED and the case IDs in affectedCaseIds, and nothing changes. To confirm it, send the same change again with force: true.Matching and single use
A completed case approves one request from one wallet. Withauthenticated_approval evidence, Para matches the request by its intent and ignores the fields that change when your app rebuilds the transaction:
Other requests, and every request under
transaction_signature evidence, must match the approved request exactly.
To use the case for a rebuilt transaction, send it as the same request. On REST, reuse the Idempotency-Key header of the first attempt. In the client SDKs, pass the case ID as resumeApprovalCaseId to signMessage or signTransaction.
A case can be used once, within seven days of when it opened. Para reserves the case when signing starts and consumes it when the signature is produced, so signing the same request again needs a new case. Once signing has started with a case, a retry must match the reserved request exactly.