Skip to main content
In a React Native app, the signed-in user is the initiator, and approvers decide in your app or on the Para Portal. The review page opens through the handler you set with setTransactionReviewHandler. The examples call the Para client, and the matching hooks, such as useInitiatedApprovalCases and useCancelApprovalCase, ship in @getpara/react-native-wallet. Require one or more people to approve a request before Para signs it. This page shows how to add an approval requirement to a policy, give people the right to approve, and follow a case from the request to the signature.
Build and test approvals with a Development key, which uses Para’s beta environment. An approval requirement reaches production with the rest of its policy, through a production review.

How conditional approvals work

A rule with effect: "require_approval" and an approvalRequirement holds every request it matches. Para opens an approval case for the request, and eligible people approve or reject it. Once enough of them approve, the same request can sign. Requesters, approvers and configuration managers are Para users with a wallet connected to your app. They act while signed in to your app, or from a Para Portal link that your app opens for them. Guardrails and Requests can both require approvals. In a Request, an approval rule applies only after the user consents to the policy, the same as its other rules.

Set up an approval requirement

Setup spans the policy, your backend and each wallet the requirement should cover. An authorization scope ties them together: it is an ID you choose, such as treasury, that groups approvers and wallets.
1

Check that conditional approvals are on for your app

Para turns conditional approvals on for each app. Until they are on, saving a policy that has an approvalRequirement fails with permissions_v2_extension_unavailable. Contact Para if you get that code.
2

Add the requirement to a rule

Add effect: "require_approval" and an approvalRequirement to the rule that should pause signing:
Stages run in order, up to five. Each stage sets requiredApprovals, from 1 to 25 or "all", and eligibility, a list of roles or attributes; matching any one entry makes a person eligible. With "all", the stage needs every eligible person, so it works only while at most 25 people are eligible. To add stages for larger amounts, set amountFact on the requirement and a minimumAmount on each later stage.
3

Activate the policy

Para accepts assignments only for roles and attributes that an active policy declares, so activate the policy before you assign anyone.
4

Assign approvers and bind wallets

From your backend, give each approver a role or attribute in the scope, then bind every wallet the requirement covers to the same scope. Both calls use the REST SDK and your secret API key:
Use expectedRevision: 0 for the first write. For later changes, read the record and pass its current revision. Approvers must have a wallet connected to your app; assigning a role to anyone else returns 400 with user is not linked to this app.
A request that matches the rule fails instead of opening a case when the setup is incomplete. A wallet with no scope binding returns 403 FORBIDDEN. When the first stage has no eligible people, fewer than its requiredApprovals, or more than 25 with "all", the request returns 400 with conditional approval stage exceeds supported limits. More than 100 eligible people returns 400 with approval stage resolves to more than 100 eligible people. The requester never counts as eligible, and neither does the initiator outside signature collections.
Para checks each later stage when the stage before it completes, and leaves out everyone who has already approved the case. If the later stage is left with too few eligible people, or more than 25 with "all", the case ends as invalidated with terminalReason stage_snapshot_invalid. If it would have more than 100, the approval that completes the earlier stage fails with 400 and approval stage resolves to more than 100 eligible people, and the case stays pending.For example, if the only person with the cfo role also holds finance and approves the first stage, a second stage for cfo has nobody left. Give each stage enough people who do not also approve the stages before it.
Assignments, wallet bindings and approval settings belong to the environment where you write them. After your policy is published to production, repeat them with your production secret key.
The approval requirements reference covers every field, including amount bands and adjustable approval settings.

Require approval for Safe payments

When Para wallets own a Safe, Para can read the payment inside a Safe transaction and hold it for approval. Rules read it with the Safe payment facts: request.safe.to for the recipient, request.safe.value.* for a payment in the chain’s native token and request.safe.token.* for an ERC-20 transfer. They apply to two EVM actions: execute_safe_transaction, the execTransaction call that runs a Safe transaction, and sign_typed_data, an owner’s signature over the Safe transaction.
Para reads a Safe transaction as a payment only when it makes a single call (operation 0) that sends the native token or calls transfer or transferFrom on an ERC-20 token, the call does not target the Safe itself, and gasPrice is 0. Keep these rules in mind:
  • Native and token payments use separate facts. Write one rule for each, because a rule that reads both fails closed on the one the payment does not use.
  • On execute_safe_transaction, request.value.baseUnits is only the value attached to the outer execTransaction call. Constrain it as well if your owners could attach value.
  • To add stages for larger Safe payments, set amountFact to request.safe.value.baseUnits or request.safe.token.value.baseUnits.
  • In a case, request.safePayment describes the payment with kind (native or erc20), safe, to, baseUnits and asset, plus from, the account the tokens are pulled from, for an ERC-20 transferFrom. The Portal approval page shows it under Safe payment.
A rule that reads Safe payment facts denies every Safe transaction Para cannot read as a single payment, such as a DelegateCall, a MultiSend batch, a call to another Safe, a call to the Safe itself (for example, adding an owner) or a payment that refunds gas. Para fails closed so that a payment cannot hide inside a batch.An owner wallet can also approve a Safe transaction directly on chain by calling approveHash on the Safe. Para reads that as call_contract, so Safe payment facts and Safe spending limits never see it. To stop it, deny call_contract when request.target is the Safe’s address. Direct calls to the Safe shows the rule.On sign_typed_data, Safe facts exist only for Safe transactions, so a rule that reads them also denies every other typed data request. Combine the Safe condition with request.typedData.primaryType in an all condition:
The Para client and server SDKs, including their viem, ethers and wagmi integrations, sign EIP-712 typed data and EIP-7702 authorizations as a message over their hash, so Para evaluates those requests as sign_message. The sign_typed_data and sign_authorization actions, and the facts Para reads from typed data and authorizations, such as request.typedData.primaryType, apply only to the REST API’s sign-typed-data and sign-authorization routes, which the REST SDK calls.
Safe spending limits use the metered facts safe.spend.native and safe.spend.erc20, which count spending per Safe across all of its Para owners. Usage reads return Safe meters only when you pass the Safe’s address as safeAddress. The Safe payment facts reference covers every Safe fact and how Safe spend is counted.

Request a signature that needs approval

The user who signs is the initiator. When a Request rule requires approval, the signing call does not fail: Para holds the transaction as a pending transaction, opens a case for it and sends the user to the review page. While approvers decide, that page shows Waiting for required approvals. Refresh after an approver has decided. Its Cancel button only closes the page; to withdraw the request, cancel the case.
If the user chose to review each transaction when they consented, they review the transaction first, and the case opens after they approve it. The signing call then throws TransactionReviewTimeout straight away, so the waiting outcomes below do not apply: follow the case, then continue after approval with resumeApprovalCaseId.
Para Portal transaction review showing Waiting for required approvals with Retry and Cancel

Track the case

The signing call waits only as long as the review window, which lasts at least five minutes. Pass a larger timeoutMs to wait longer. When the window ends, the call throws TransactionReviewTimeout, which carries the pendingTransactionId. Approvals often take longer, so treat the timeout as an expected outcome and follow the case instead. The case’s request.id is the pending transaction ID. listInitiatedApprovalCases() returns the user’s cases, newest first:
Read a single case with getInitiatedApprovalCase(caseId). The hooks useInitiatedApprovalCases and useInitiatedApprovalCase wrap the same calls. While the signing call is still waiting, the case decides how it ends:
An invalidation or expiry does not end a signing call that is still waiting. Watch the case status, and when it is expired or invalidated, return true from the signing call’s isCanceled callback to stop waiting. The call then throws TransactionReviewTimeout, so catch it before you tell the user.
When the user did not review the transaction first, the approvers’ decision stands once the case completes. If the user selects Reject on the review page after that, Para refuses it with PENDING_TRANSACTION_ALREADY_APPROVED. To stop a request, cancel the case while it is still pending.

Continue after approval

If the signing call ended before the case completed, sign again and pass the case ID as resumeApprovalCaseId. signMessage takes the same option.
With resumeApprovalCaseId, you can rebuild the transaction before you sign again, for example because the wallet’s nonce moved on while approvers decided. An EVM transaction can change its nonce, gas limit and fees. A Stellar transaction can change its fee, sequence number and signatures, the outer fee of a fee bump, and every precondition: time bounds, ledger bounds, minSeqNum, minSeqAge, minSeqLedgerGap and extraSigners. Every other field is part of the approval, including maxFeePerBlobGas on an EIP-4844 blob transaction and, on a Soroban transaction, its resource fee, its declared resources and the nonce and signature expiration ledger of each authorization entry. Changing one of them needs a new approval. A Solana, Cosmos or Sui transaction must be resent unchanged, and any other change returns 409 with request already has a different conditional approval case. Co-signed cases under transaction_signature evidence, and any retry after signing has started, must match the approved request exactly.
An approval under authenticated_approval evidence sets no fee ceiling. A rebuilt transaction can carry a higher network fee than the first attempt, so it can spend more of the fee payer’s native balance. Para evaluates every rebuilt request against your current policies, and EVM rules on request.gas.* facts still apply. No fact holds an EVM transaction’s total fee or a Stellar transaction’s fee, and fees never count toward spending limits.
Without resumeApprovalCaseId, Para continues a case only for the identical encoded transaction, and it opens a new case when the earlier one has ended, even after a rejection. Pass the case ID whenever you continue a request, so a rejected request does not reach approvers a second time by accident.

Let approvers decide

Approvers act while signed in to Para through your app. They can decide in your own interface with the SDK, or on a Para Portal page that your app opens for them.

Check whether the user can decide

listAccessibleApprovalCases() returns the cases the signed-in user can see, and getAccessibleApprovalCase(caseId) returns one of them. Each case includes a decision object for the current user:
Show approve and reject controls only when decision.canDecide is true. A refused decision returns 403 with the message approval decision is not allowed and a code that names why, such as ACTOR_NOT_ELIGIBLE or CASE_TERMINAL. After a failed decision, read the case again and render its current decision.

Decide in your interface

Read the case, check decision, then record the decision for the signed-in approver:
React Native apps import the same hooks from @getpara/react-native-wallet. Use rejectApprovalCase(caseId) the same way to reject. Each approval counts once toward its stage. The case completes when every stage that applies has its approvals, and a single rejection from an eligible approver ends it as rejected.

Send approvers to the Para Portal

getApprovalCaseReviewUrl({ caseId }) returns a link to the Portal approval page for the signed-in user. It reads the case first, so it throws for a user who cannot view the case. For the user who made the request, the error’s code is INITIATOR_CANNOT_REVIEW; show them that the request is waiting for approvers instead. The method returns the URL without opening it.
The page is titled Review transaction approval and shows a summary of the request with Approve and Reject. After a decision it shows Approval recorded. or Request rejected. with Done. While the case is pending, a user who cannot decide sees This request is not available for you to approve. with Refresh. Once the case has ended, the page shows its status instead, such as This request is rejected., and a cancelled case shows This request was cancelled by the requester or the app. If the requester opens a link you shared with the whole team, the page shows You requested this payment, so you cannot approve it. without the decision buttons.
Para Portal approval review showing the amount, chain and Approve and Reject buttons
Cases whose requirement uses evidence: "transaction_signature" collect wallet signatures instead of approvals, and approveApprovalCase refuses them. The approval signatures page in this section covers that flow.

Tell approvers about new cases

Para does not notify approvers. Your app decides who hears about a case and how. When approvers open your app, listAccessibleApprovalCases() works as their inbox. To reach them sooner, send the case ID or the Portal link through your own notifications. On your backend, listAuthorizationScopeMembers(scopeId) lists the people who hold assignments in a scope.

Case lifecycle

A case starts as pending and ends in one of five statuses. Approvers read it with getAccessibleApprovalCase, the user who started it reads it with getInitiatedApprovalCase, and your backend reads it with getPartnerApprovalCase. These rules decide how a case progresses:
  • A case lasts seven days from when it opens, until expiresAt. A completed case can sign only before expiresAt, and only once: signing uses it up.
  • Stages run in order. currentStageIndex points to the open stage, and each entry in stages lists requiredApprovals, eligibleActorCount and approvedActorIds. With "all", the stage needs every person who was eligible when it opened, which works only while at most 25 people are eligible.
  • Para records who is eligible when each stage opens. Assigning a role later does not add that person to a stage that is already open.
  • A person counts once per case. Someone who approved an earlier stage cannot approve a later one.
  • The initiator cannot approve or reject their own case. Signature collections are the exception: an eligible initiator can add their own signature.
  • When your backend signs with initiatorUserId, Para leaves that user out of every stage, even if they hold an approver role.
  • decisions lists every vote with its stage and time.
The Portal approval page never shows expiresAt or terminalReason. If approvers need either, show them in your app from the case fields.

Cancel a pending case

Withdraw a request that is still waiting, for example when the user changes their mind.
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 returns 409 with conditional approval case is not pending. A signing call that is still waiting on the discarded transaction throws TransactionReviewDenied.

Changing a policy with open approvals

Para checks an open case against the current setup whenever the case is read, decided or used to sign. If something the case depends on has changed, the case becomes invalidated, and terminalReason says why. The common reasons are: A later stage that would have more than 100 eligible people does not end the case. The approval that completes the stage before it fails with 400 and approval stage resolves to more than 100 eligible people, and the case stays pending until the assignments change or it expires.
An approval case that has not been used to sign is invalidated when the active policies that apply to its request change, for example when a policy is replaced, activated or turned off. An invalidated case cannot complete, so start a new request to open a new case.
Signature collections work differently. A policy change ends a collection only when its own policy version stops applying, for example because it was replaced or turned off, and its terminalReason is then requirement_unavailable.

Confirm changes that affect open cases

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.
The REST SDK throws this refusal as a ParaRestError, and the client SDKs that configuration managers use throw PermissionsV2ApprovalCasesAffectedError. Both carry affectedCaseIds. The check covers replacing, activating and turning off a policy, changing a user’s assignments, moving a wallet to another scope, and saving or resetting adjustable approval settings. In the Developer Portal and the CLI, you confirm the change when Para asks: Changes that happen without a write, such as a policy’s time window opening or closing, invalidate cases without asking.

Open Portal pages from React Native

React Native has no popup, so open the approval page with the same in-app browser your review handler uses: