What Guardrails restrict
Your project owns some wallets outright: wallets created through the REST API, and pregenerated wallets that no user has claimed. Your backend can already sign anything with them. A Guardrail is a policy that takes cases away, so a bug or a bad input cannot make your backend sign something you never intended. Each Guardrail rule has one of these effects:
Any other
allow rule fails with guardrail_effect_invalid, because the wallet can already sign. Requests that no Guardrail blocks sign as usual. When several active Guardrails match a request, they all apply: deny wins over require_approval, so a block in any of them stops the request.
Guardrails apply only while a wallet belongs to your project. When a user claims a pregenerated wallet, it becomes user-owned, and Requests govern it instead. See How permissions work.
Permissions supports EVM, Solana, Cosmos, Stellar and Sui wallets. Each policy targets one wallet type, so create a separate policy for each wallet type you want rules for. A policy covers every network of its wallet type unless you list specific networks.
Before you start
You need:- A Development API key. Policies are written and tested on Development keys, which use Para’s beta environment. Production keys are read-only in the Developer Portal.
- App-owned wallets created with that key, and a server that signs with them.
- For
require_approvalrules only:- Para must enable conditional approvals for your key. Until then, saving a policy with an approval requirement fails with
permissions_v2_extension_unavailable. - Each wallet must be bound to an authorization scope. A request that needs approval from an unbound wallet returns
403 FORBIDDEN.
- Para must enable conditional approvals for your key. Until then, saving a policy with an approval requirement fails with
Choose how your server signs
Guardrails apply to app-owned wallets whichever SDK your server signs with, and both SDKs report the same outcomes:A server that signs with a session imported from a signed-in user is signing that user’s wallet. Requests apply to it, not Guardrails. See How permissions work, and Review links on a server for the review that follows.
REST SDK outcomes
The REST SDK calls the REST API, so your server receives these responses. Para checks every request to the signing routes (sign-transaction, sign-message, sign-typed-data, sign-authorization and sign-raw) and to transfer against the wallet’s active Guardrails before it signs. Each response has one of four outcomes:
Send every signing and transfer request with an
Idempotency-Key taken from your own records, such as a payment ID. Para ties an approval to that key, and after a request succeeds it returns the stored response for repeats of the same key and body for 24 hours.
ParaRestError carries the HTTP status, the code and the parsed response on body.
Signed
A request that no Guardrail stops signs as usual and returns the route’s normal response. Repeating it with the sameIdempotency-Key and body returns the stored response instead of signing again.
While Permissions is on, raw signatures follow the same defaults as other signing requests: an app-owned wallet signs them, and a user-owned wallet sends them to the user for review. Rules that name the
raw_signature action, and rules with "actions": "all", can change that. Raw signatures are sign-raw requests, messages from Solana, Stellar and Sui wallets, and REST sign-message requests from a Cosmos wallet that do not pass walletType: "EVM". The SDKs sign messages from Cosmos wallets as EVM sign_message requests. Raw signatures carry no network a selector can name, so only a policy without chainId governs them. A rule that names raw_signature can read only para.wallet.* facts. The Developer Portal forms cannot create one, so write it in policy JSON, with Edit JSON in the Developer Portal or with the CLI.Denied
Para refuses to sign and returns403. Nothing is signed or broadcast.
403 POLICY_DENIED
deniedBy holds one entry. policyId and ruleId appear when a rule decided the outcome. Common reason values:
Approval required
When the deciding rule is arequire_approval rule with an approval requirement, Para opens an approval case instead of signing and returns 403:
403 POLICY_REVIEW_REQUIRED
string
The approval case for this request. Read its progress with
getPartnerApprovalCase(approvalCaseId) or GET /v1/permissions-v2/approval-cases/{caseId}.string
pending for a new case. When you resend a request whose case has ended, it can hold that case’s final status: completed, rejected, expired, invalidated or cancelled.string
The rule that requires approval, as
matched:<layer>:<ruleId>.string
The ID of the policy decision Para recorded for this request. Include it when you contact Para about a decision.
A deciding
require_approval rule without an approvalRequirement returns an ordinary reviewId. Approve or reject that review from your backend with your server secret using the review routes. Once its status is approved, resend the identical body with the same Idempotency-Key while the approval is unused, unexpired and still matches the request and policy binding.Sponsored transfers do not support approval rules. A sponsored
transfer that matches one returns 400 SPONSORSHIP_CONDITIONAL_APPROVAL_UNSUPPORTED.Name the user behind a request
When your backend signs on behalf of one of your users, addinitiatorUserId with that user’s Para user ID to the signing or transfer request. Para records the user as the requester of any approval case the request opens, and that user cannot approve or reject the case, even if the approval requirement names them.
400 INVALID_REQUEST and the message user is not linked to this app. Send the same initiatorUserId every time you resend the request.
Continue after approval
For a response withapprovalCaseId, Para ties the case to the request’s Idempotency-Key, or to a hash of the request body when you send no key. The case stays open for up to 7 days.
1
Store the case with the request
Save
approvalCaseId next to the Idempotency-Key and the request body.2
Wait for the approvers
Poll
getPartnerApprovalCase(approvalCaseId) until status is completed.3
Resend the identical request
Send the same body with the same
Idempotency-Key. Para evaluates it again, finds the completed case and signs. A completed case can be used for one signature.POLICY_REVIEW_REQUIRED response that carries approvalCaseId, sending a different body or a different initiatorUserId with the same Idempotency-Key returns 409 CONFLICT with the message request already has a different conditional approval case.
Cancel a pending case
If you no longer need the request, cancel its case so approvers cannot approve it. CallcancelPartnerApprovalCase(approvalCaseId) in the REST SDK, or send POST /v1/permissions-v2/approval-cases/{caseId}/cancel.
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.
The new attempt needs a new Idempotency-Key. Cancelling a case that is no longer pending returns 409 CONFLICT with the message conditional approval case is not pending.
Spend accounting failed
Para could not record spend against a spending limit, for a reason other than the limit itself. It returns500 with code SPEND_ACCOUNTING_FAILED and the message Windowed spend accounting failed, and nothing is signed. Retry with backoff, using the same Idempotency-Key and body.
A request that would exceed a limit is a denial with the reason windowed-spend-limit-exceeded, not this error.
Server SDK outcomes
The server SDK signs a pregenerated wallet after you load its user share withsetUserShare. When no Guardrail stops the request, signTransaction and signMessage return { signature }, as they do without Permissions. Errors from its signing worker keep their code, status and data, so your code can act on each outcome.
Handle a denial
When a policy denies the request, the signing call rejects instead of opening a review. Para’s refusal hascode: "POLICY_DENIED" and status: 403. When a rule decided the denial, data.deniedBy names it with the same { policyId, ruleId, reason } entries the REST API returns. A spending limit reached while Para reserved the spend reports windowed-spend-limit-exceeded.
The React Native SDK and the server SDK reject with an error object that carries this code, status and data. The object is not always an Error instance, so check its code property rather than its class.
Handle a required approval
When a decidingrequire_approval rule has an approvalRequirement, Para opens an approval case and the signing call throws an error with code: "POLICY_REVIEW_REQUIRED" and status: 403. error.data holds the same approvalCaseId, approvalCaseStatus, artifactId and reason fields as the REST response.
When the deciding rule requires approval without an
approvalRequirement, error.data carries an ordinary reviewId. Your backend approves or rejects it with your app’s server secret through the routes in Pending transaction review. After approved, retry identical SDK signing input without resumeApprovalCaseId, only while the approval is unused and unexpired and the request and policy binding still match.payouts.ts
approvalCaseId with the transaction. To read the case’s progress, call getPartnerApprovalCase(approvalCaseId) on a @getpara/rest-sdk client created with your secret key. Then call signPayout again with the identical transaction and the case ID. The result depends on the case:
To try again after a case has ended or been used, sign the transaction without
resumeApprovalCaseId, which opens a new case. To drop a request while its case is pending, cancel the case with cancelPartnerApprovalCase(approvalCaseId) on the same REST SDK client.
Pass initiatorUserId to signTransaction or signMessage to name the Para user your server is acting for. It works as it does on the REST API: the user must be linked to your app, and cannot approve or reject the case. Para reads it only for wallets your project owns.
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.Review links on a server
When your server signs for a signed-in user, any review opens in the Para Portal. A server has no window to open it in, so the defaultmanaged review mode fails with OpenPopup is not implemented in the ServerUtils class. Choose one of these instead:
- Set
transactionReviewMode: "handoff"when you create the client. The signing call returns the review link instead of waiting, and you send it to the user. - Keep
managedmode and passonTransactionReviewUrlto the signing call. The SDK hands you the link, then waits for the user’s decision as it would in a browser.
server.ts
reviewUrl to the user. Once they approve, sign the identical transaction again to get the signature. Wallet integrations that must return a signature, such as the viem and ethers signers, throw TransactionReviewRequired in handoff mode instead, with the same transactionReviewUrl and pendingTransactionId.
Add a Guardrail
Create the policy on a Development key, run every rule against beta with your server code, then request a production review. Start in the Developer Portal; move to JSON for rules the forms cannot express.Your production server signs as it does today until Para publishes a reviewed policy to your Production key. Guardrails you activate on a Development key change signing on beta only.