> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails for server-side wallets

> Limit what your Node.js server can sign with app-owned wallets, and handle each policy outcome in the REST SDK and the server SDK

export const PermissionsCards = ({basePath, pages, cols = 2, horizontal = false}) => {
  const ImageCard = Card;
  const hrefOverrides = {
    "/v3/react/guides/permissions": {
      "conditional-approvals": "/v3/react/guides/conditional-approvals"
    },
    "/v3/rest/permissions": {
      "backend": "/v3/rest/permissions-administration"
    }
  };
  const guardrailBases = ["/v3/rest/permissions", "/v3/server/guides/permissions"];
  const ownership = guardrailBases.includes(basePath) ? 'guardrail' : 'request';
  const overviewImages = {
    "/v3/rest/permissions": {
      "imgUrl": "/images/v3/cards/rest-guardrails.png",
      "horizontalImgUrl": "/images/v3/cards/rest-guardrails-horizontal.png"
    },
    "/v3/server/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-server-guardrail-signing.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-server-guardrail-signing-horizontal.png"
    },
    "/v3/react/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-get-policy-consent-url.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-get-policy-consent-url-horizontal.png"
    },
    "/v3/react-native/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-get-policy-consent-url.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-get-policy-consent-url-horizontal.png"
    }
  };
  const registry = {
    overview: {
      title: "Permissions overview",
      description: "Return to the start of the Permissions path.",
      ...overviewImages[basePath]
    },
    "developer-portal": {
      title: "Create policies in the Developer Portal",
      description: "Build a policy with the Portal forms and activate it on a beta key.",
      imgUrl: "/images/v3/cards/developer-portal-policy-form.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-form-horizontal.png"
    },
    "json": {
      title: "Write policy JSON",
      description: "Move to JSON for policies the forms cannot express.",
      imgUrl: "/images/v3/cards/developer-portal-policy-json.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-json-horizontal.png"
    },
    "testing": {
      title: "Test your policies",
      description: "Exercise every rule on beta before you ship.",
      imgUrl: "/images/v3/cards/concept-policy-testing.png",
      horizontalImgUrl: "/images/v3/cards/concept-policy-testing-horizontal.png"
    },
    "production": {
      title: "Go to production",
      description: "Request a production review and publish your policy.",
      imgUrl: "/images/v3/cards/concept-policy-publication.png",
      horizontalImgUrl: "/images/v3/cards/concept-policy-publication-horizontal.png"
    },
    "conditional-approvals": {
      title: "Conditional approvals",
      description: "Require people to approve a request before it signs.",
      imgUrl: "/images/v3/cards/portal-conditional-approval-case.png",
      horizontalImgUrl: "/images/v3/cards/portal-conditional-approval-case-horizontal.png"
    },
    "approval-signatures": {
      title: "Collect approval signatures",
      description: "Collect signatures from approvers for multisig workflows.",
      imgUrl: "/images/v3/cards/sdk-get-approval-signatures.png",
      horizontalImgUrl: "/images/v3/cards/sdk-get-approval-signatures-horizontal.png"
    },
    "adjustable-parameters": {
      title: "Adjustable parameters",
      description: "Let users set their own limits and managers adjust approval settings.",
      imgUrl: "/images/v3/cards/portal-user-policy-limits.png",
      horizontalImgUrl: "/images/v3/cards/portal-user-policy-limits-horizontal.png"
    },
    "lifecycle": {
      title: "Manage the policy lifecycle",
      description: "Version, activate, replace and retire policies.",
      imgUrl: "/images/v3/cards/developer-portal-policy-versions.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-versions-horizontal.png"
    },
    "backend": {
      title: "Manage permissions from your backend",
      description: "Assign approvers and read policy state with your server secret.",
      imgUrl: "/images/v3/cards/rest-permissions-administration.png",
      horizontalImgUrl: "/images/v3/cards/rest-permissions-administration-horizontal.png"
    },
    templates: ownership === "guardrail" ? {
      title: "Guardrail templates",
      description: "Start from a tested Guardrail policy.",
      imgUrl: "/images/v3/cards/concept-guardrail-templates.png",
      horizontalImgUrl: "/images/v3/cards/concept-guardrail-templates-horizontal.png"
    } : {
      title: "Request templates",
      description: "Start from a tested Request policy.",
      imgUrl: "/images/v3/cards/concept-request-templates.png",
      horizontalImgUrl: "/images/v3/cards/concept-request-templates-horizontal.png"
    },
    "guardrail:templates/spending-caps": {
      title: "Spending caps",
      description: "Cap native and token transfers per transaction or per day.",
      imgUrl: "/images/v3/cards/concept-spending-limits.png",
      horizontalImgUrl: "/images/v3/cards/concept-spending-limits-horizontal.png"
    },
    "guardrail:templates/allowlists": {
      title: "Recipient and contract allowlists",
      description: "Limit transfers and contract calls to addresses you know.",
      imgUrl: "/images/v3/cards/concept-recipient-allowlists.png",
      horizontalImgUrl: "/images/v3/cards/concept-recipient-allowlists-horizontal.png"
    },
    "guardrail:templates/approval-tiers": {
      title: "Approval thresholds and tiers",
      description: "Hold large transfers for approval, or deny the largest.",
      imgUrl: "/images/v3/cards/concept-approval-tiers.png",
      horizontalImgUrl: "/images/v3/cards/concept-approval-tiers-horizontal.png"
    },
    "guardrail:templates/treasury": {
      title: "Treasury approvals",
      description: "Deny unplanned actions and approve wallet or Safe payouts in stages.",
      imgUrl: "/images/v3/cards/concept-treasury-approvals.png",
      horizontalImgUrl: "/images/v3/cards/concept-treasury-approvals-horizontal.png"
    },
    "guardrail:templates/message-signing": {
      title: "Message and raw signing",
      description: "Block or hold raw signatures, and block permits.",
      imgUrl: "/images/v3/cards/concept-signing-actions.png",
      horizontalImgUrl: "/images/v3/cards/concept-signing-actions-horizontal.png"
    },
    "request:templates/allowances": {
      title: "Transfer allowances",
      description: "Let small transfers and a daily budget sign without a prompt.",
      imgUrl: "/images/v3/cards/concept-spending-limits.png",
      horizontalImgUrl: "/images/v3/cards/concept-spending-limits-horizontal.png"
    },
    "request:templates/recipients-and-contracts": {
      title: "Known recipients and app contracts",
      description: "Cover transfers to known addresses and your contract's calls.",
      imgUrl: "/images/v3/cards/concept-recipient-allowlists.png",
      horizontalImgUrl: "/images/v3/cards/concept-recipient-allowlists-horizontal.png"
    },
    "request:templates/cosigner-approval": {
      title: "Co-signer approval",
      description: "Send large transfers to a co-signer for approval.",
      imgUrl: "/images/v3/cards/portal-conditional-approval-case.png",
      horizontalImgUrl: "/images/v3/cards/portal-conditional-approval-case-horizontal.png"
    },
    "request:templates/adjustable-limits": {
      title: "User-adjustable limits",
      description: "Let each user choose their own spending limits.",
      imgUrl: "/images/v3/cards/portal-user-policy-limits.png",
      horizontalImgUrl: "/images/v3/cards/portal-user-policy-limits-horizontal.png"
    },
    "request:templates/message-signing": {
      title: "Message signing",
      description: "Sign messages from users' wallets without a prompt.",
      imgUrl: "/images/v3/cards/concept-signing-actions.png",
      horizontalImgUrl: "/images/v3/cards/concept-signing-actions-horizontal.png"
    }
  };
  const entryFor = slug => registry[slug] || registry[`${ownership}:${slug}`];
  const hrefFor = slug => {
    if (slug === 'overview') return basePath;
    const overrides = hrefOverrides[basePath] || ({});
    return overrides[slug] || `${basePath}/${slug}`;
  };
  return <CardGroup cols={cols}>
      {pages.map(slug => {
    const entry = entryFor(slug);
    return <ImageCard key={slug} title={entry.title} description={entry.description} href={hrefFor(slug)} imgUrl={horizontal ? entry.horizontalImgUrl : entry.imgUrl} horizontal={horizontal} />;
  })}
    </CardGroup>;
};

export const Card = ({imgUrl, compactImgUrl, title, description, href, horizontal = false, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  const handleClick = e => {
    e.preventDefault();
    if (newTab) {
      window.open(href, '_blank', 'noopener,noreferrer');
    } else {
      window.location.href = href;
    }
  };
  return <div className={`not-prose relative my-2 p-[1px] rounded-xl transition-all duration-300 ${isHovered && href ? 'bg-gradient-to-r from-[#FF4E00] to-[#874AE3]' : 'bg-gray-200'}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      <a href={href} onClick={href ? handleClick : undefined} className={`not-prose flex ${horizontal ? 'flex-row' : 'flex-col'} font-normal h-full bg-white overflow-hidden w-full ${href ? 'cursor-pointer' : 'cursor-default'} rounded-[11px] no-underline`}>
        {imgUrl && <div className={`relative overflow-hidden flex-shrink-0 ${horizontal ? 'w-[30%] rounded-l-[11px]' : 'w-full'}`} onClick={e => e.stopPropagation()}>
            <picture style={{
    display: 'contents'
  }}>
              {compactImgUrl && <source media="(max-width: 639px)" srcSet={compactImgUrl} />}
              <img src={imgUrl} alt={title} className="w-full h-full object-cover pointer-events-none select-none" draggable="false" />
            </picture>
            <div className="absolute inset-0 pointer-events-none" />
          </div>}
        <div className={`flex-grow px-6 py-5 ${horizontal ? 'w-[70%]' : 'w-full'} flex flex-col ${horizontal && imgUrl ? 'justify-center' : 'justify-start'}`}>
          {title && <h2 className="font-semibold text-base text-gray-800 m-0">{title}</h2>}
          {description && <div className={`font-normal text-gray-500 re leading-6 ${horizontal || !imgUrl ? 'mt-0' : 'mt-1'}`}>
              <p className="m-0 text-xs">{description}</p>
            </div>}
        </div>
      </a>
    </div>;
};

Guardrails limit what your server can sign with the wallets your project owns.

## 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:

| Effect | What Para does with a matching request |
| - | - |
| `deny` | Refuses to sign. |
| `require_approval` | Holds the request until the people named in the rule's approval requirement approve it. |
| `allow`, with `actions` set to exactly `["raw_signature"]` | Nothing beyond the default, because app-owned wallets already sign raw signatures. Para still accepts this rule so older Guardrails stay valid. |

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.

<Note>
  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](/v3/concepts/permissions).
</Note>

<Note>
  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.
</Note>

## 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_approval` rules 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`.

<Warning>
  Guardrails protect you from mistakes in your own code. They are not a defense against a leaked secret key, so keep the key secret and rotate it if it is exposed.
</Warning>

## Choose how your server signs

Guardrails apply to app-owned wallets whichever SDK your server signs with, and both SDKs report the same outcomes:

| | REST SDK (`@getpara/rest-sdk`) | Server SDK (`@getpara/server-sdk`) |
| - | - | - |
| Wallets | Wallets created through the REST API | Pregenerated wallets whose user share your server holds |
| A rule denies the request | `403 POLICY_DENIED`, with the rule in `deniedBy` | An error with `code: "POLICY_DENIED"`, with the rule in `error.data.deniedBy` |
| A rule requires approval | `403 POLICY_REVIEW_REQUIRED`, with `approvalCaseId` when the rule has an `approvalRequirement`, or `reviewId` otherwise | An error with `code: "POLICY_REVIEW_REQUIRED"`, with `error.data.approvalCaseId` when the rule has an `approvalRequirement`, or `error.data.reviewId` otherwise |
| Continue once approved | For a case, resend with the same `Idempotency-Key`. For an ordinary review, have your backend resolve `reviewId`, then resend the identical request. | For a case, sign again with `resumeApprovalCaseId`. For an ordinary review, have your backend resolve `reviewId`, then retry identical signing input without `resumeApprovalCaseId`. |
| Name the user behind a request | `initiatorUserId` in the request body | `initiatorUserId` in the signing call |

<Tip>
  The REST SDK also makes retries safe with idempotency keys. [Migrate from pregenerated wallets](/v3/rest/migrate-from-sdk-pregen) covers the move.
</Tip>

<Info>
  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](/v3/concepts/permissions), and [Review links on a server](#review-links-on-a-server) for the review that follows.
</Info>

## 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:

| Outcome | Response | Next step |
| - | - | - |
| [Signed](#signed) | The route's normal `200` response | None. |
| [Denied](#denied) | `403` with code `POLICY_DENIED` | Read `deniedBy[].reason`. Most denials repeat until the request or the policy changes. A spending-limit denial, or one caused by a value Para could not fetch, can succeed on a later try. |
| [Approval required](#approval-required) | `403` with code `POLICY_REVIEW_REQUIRED` | For `approvalCaseId`, wait for the approvers; for `reviewId`, resolve the ordinary review through your backend. After approval, resend the identical body with the same `Idempotency-Key`. |
| [Spend accounting failed](#spend-accounting-failed) | `500` with code `SPEND_ACCOUNTING_FAILED` | Retry with backoff and the same `Idempotency-Key`. |

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.

<CodeGroup>
  ```typescript REST SDK theme={null}
  import { ParaRestClient, ParaRestError } from "@getpara/rest-sdk";

  const para = new ParaRestClient({ apiKey: process.env.PARA_API_KEY!, env: "BETA" });

  export async function sendPayment(paymentId: string, walletId: string, to: string, value: string) {
    try {
      const result = await para.transfer(walletId, { to, value, chainId: 11155111 }, { idempotencyKey: paymentId });
      return { status: "signed", result } as const;
    } catch (error) {
      if (!(error instanceof ParaRestError)) throw error;
      if (error.code === "POLICY_DENIED") return { status: "denied", body: error.body } as const;
      if (error.code === "POLICY_REVIEW_REQUIRED") return { status: "awaiting-approval", body: error.body } as const;
      throw error;
    }
  }
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.beta.getpara.com/v1/wallets/$WALLET_ID/transfer" \
    -H "X-API-Key: $PARA_API_KEY" \
    -H "Idempotency-Key: payment-4821" \
    -H "Content-Type: application/json" \
    -d '{ "to": "0x1111111111111111111111111111111111111111", "value": "1000000000000000", "chainId": 11155111 }'
  ```
</CodeGroup>

`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 same `Idempotency-Key` and body returns the stored response instead of signing again.

<Note>
  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](/v3/cli/permissions).
</Note>

### Denied

Para refuses to sign and returns `403`. Nothing is signed or broadcast.

```json 403 POLICY_DENIED theme={null}
{
  "deniedBy": [
    {
      "policyId": "treasury-limits",
      "ruleId": "block-large-transfers",
      "reason": "matched:partner:block-large-transfers"
    }
  ],
  "code": "POLICY_DENIED",
  "message": "Transaction denied by policy"
}
```

`deniedBy` holds one entry. `policyId` and `ruleId` appear when a rule decided the outcome. Common `reason` values:

| `reason` | Cause | What to do |
| - | - | - |
| `matched:partner:<ruleId>` | One of your `deny` rules matched. | Change the request, or the policy. |
| `matched:para:<ruleId>` | A Para rule matched. | Change the request. |
| `default-deny` | No rule decided the request and nothing allowed it. | Add a rule that covers the request, or change the request. |
| `review-rejected` | An approver rejected the approval case for this request. | Do not resend it. Start a new request if it is still needed. |
| `windowed-spend-limit-exceeded` | The request would take a spending limit past its cap for the current window. | Send less, or wait until the window resets. `getPartnerPolicyUsage` returns each limit's `resetAt`. |
| `missing-fact`, `unknown-fact` | A rule needed a value Para could not read from this request. | Change the rule, or send a request Para can read. |
| `stale-fact`, `error-fact`, `resolver-error` | Para could not fetch a value a rule needed. | Retry later. |
| `network-unverifiable`, `coin-evidence-unverifiable` | Para could not prove the request's network, or the coins a Sui transaction spends. | Send a request whose network Para can prove. |

<Warning>
  Para does not fall back to its defaults when it cannot read part of a request. If a rule's condition needs a value Para cannot read, that rule is indeterminate, and the request is denied with a reason such as `missing-fact` unless another matching rule has the same or a stronger effect. Once an approval completes, the approved rule no longer counts as a matching rule, so an indeterminate rule can still deny the approved request. Write conditions on values that the rule's actions always provide; the [facts reference](/v3/references/permissions/facts) lists them.
</Warning>

### Approval required

When the deciding rule is a `require_approval` rule with an approval requirement, Para opens an approval case instead of signing and returns `403`:

```json 403 POLICY_REVIEW_REQUIRED theme={null}
{
  "approvalCaseId": "ca_sha256:5e0c4f2a9b1d...",
  "approvalCaseStatus": "pending",
  "artifactId": "sha256:9b2e71c4d0a6...",
  "reason": "matched:partner:approve-large-transfers",
  "code": "POLICY_REVIEW_REQUIRED",
  "message": "Transaction requires policy review"
}
```

<ResponseField name="approvalCaseId" type="string">
  The approval case for this request. Read its progress with `getPartnerApprovalCase(approvalCaseId)` or `GET /v1/permissions-v2/approval-cases/{caseId}`.
</ResponseField>

<ResponseField name="approvalCaseStatus" type="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`.
</ResponseField>

<ResponseField name="reason" type="string">
  The rule that requires approval, as `matched:<layer>:<ruleId>`.
</ResponseField>

<ResponseField name="artifactId" type="string">
  The ID of the policy decision Para recorded for this request. Include it when you contact Para about a decision.
</ResponseField>

The approvers named in the rule's approval requirement decide in the Para Portal. Your secret key can read and cancel the case, but it cannot approve on anyone's behalf.

<Note>
  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](/v3/references/permissions/errors#pending-transaction-review). 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.
</Note>

<Note>
  Sponsored transfers do not support approval rules. A sponsored `transfer` that matches one returns `400 SPONSORSHIP_CONDITIONAL_APPROVAL_UNSUPPORTED`.
</Note>

### Name the user behind a request

When your backend signs on behalf of one of your users, add `initiatorUserId` 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.

```typescript theme={null}
await para.transfer(walletId, { to, value, chainId: 11155111, initiatorUserId: userId }, { idempotencyKey: paymentId });
```

The user must already be linked to your app. Otherwise the request fails with `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 with `approvalCaseId`, 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.

<Steps>
  <Step title="Store the case with the request">
    Save `approvalCaseId` next to the `Idempotency-Key` and the request body.
  </Step>

  <Step title="Wait for the approvers">
    Poll `getPartnerApprovalCase(approvalCaseId)` until `status` is `completed`.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Warning>
  A case that ends without approval cannot sign. If the case is `rejected`, `expired`, `invalidated` or `cancelled`, resending with the same key is refused again. To try again, send the request with a new `Idempotency-Key`, which opens a new case.

  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.
</Warning>

After a `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. Call `cancelPartnerApprovalCase(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 returns `500` 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](#denied) with the reason `windowed-spend-limit-exceeded`, not this error.

<CardGroup cols={1}>
  <Card title="Permissions errors" description="Every Permissions error code, with its status, body and the handling Para expects." href="/v3/references/permissions/errors" imgUrl="/images/v3/cards/concept-error-handling-horizontal.png" horizontal />
</CardGroup>

## Server SDK outcomes

The server SDK signs a pregenerated wallet after you load its user share with `setUserShare`. 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 has `code: "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 deciding `require_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.

<Note>
  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](/v3/references/permissions/errors#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.
</Note>

```typescript payouts.ts theme={null}
import { Environment, Para as ParaServer } from "@getpara/server-sdk";

const para = new ParaServer(Environment.BETA, process.env.PARA_API_KEY!);

function readApprovalCase(error: object) {
  if (!("code" in error) || error.code !== "POLICY_REVIEW_REQUIRED") return undefined;
  if (!("data" in error) || typeof error.data !== "object" || error.data === null) return undefined;
  const data = error.data;
  if (!("approvalCaseId" in data) || typeof data.approvalCaseId !== "string") return undefined;
  if (!("approvalCaseStatus" in data) || typeof data.approvalCaseStatus !== "string") return undefined;
  return { approvalCaseId: data.approvalCaseId, approvalCaseStatus: data.approvalCaseStatus };
}

export async function signPayout(walletId: string, rlpEncodedTxBase64: string, resumeApprovalCaseId?: string) {
  try {
    const result = await para.signTransaction({ walletId, rlpEncodedTxBase64, chainId: "11155111", resumeApprovalCaseId });
    if (!("signature" in result)) throw new Error("Expected a signature for an app-owned wallet");
    return { status: "signed", signature: result.signature } as const;
  } catch (error) {
    const approvalCase = typeof error === "object" && error !== null ? readApprovalCase(error) : undefined;
    if (approvalCase) return { status: "awaiting-approval", ...approvalCase } as const;
    throw error;
  }
}
```

Store the `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:

| Case status | Signing again with `resumeApprovalCaseId` |
| - | - |
| `pending` | Throws again. Para's refusal has code `POLICY_REVIEW_REQUIRED` and `approvalCaseStatus: "pending"`. |
| `completed` | Signs, once per case. A case that was already used, or whose approval expired, throws instead. Para's refusal has code `POLICY_REVIEW_REQUIRED` and `approvalCaseStatus: "completed"`. |
| `rejected` | Throws. Para's refusal has code `POLICY_DENIED` and reason `review-rejected`. |
| `expired`, `invalidated` or `cancelled` | Throws. Para's refusal has code `POLICY_REVIEW_REQUIRED`, and its `approvalCaseStatus` shows how the case ended. |

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.

<Warning>
  Send the same `initiatorUserId` on every retry and resume of the transaction. A different or missing value fails with status `409`.
</Warning>

<Note>
  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.
</Note>

### 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 default `managed` 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 `managed` mode and pass `onTransactionReviewUrl` to the signing call. The SDK hands you the link, then waits for the user's decision as it would in a browser.

```typescript server.ts theme={null}
import { Environment, Para as ParaServer } from "@getpara/server-sdk";

const para = new ParaServer(Environment.BETA, process.env.PARA_API_KEY!, {
  transactionReviewMode: "handoff",
});

export async function signForUser(serializedSession: string, walletId: string, rlpEncodedTxBase64: string) {
  await para.importSession(serializedSession);
  const result = await para.signTransaction({ walletId, rlpEncodedTxBase64, chainId: "11155111" });

  if ("signature" in result) {
    return { status: "signed", signature: result.signature } as const;
  }
  return {
    status: "needs-review",
    reviewUrl: result.transactionReviewUrl,
    pendingTransactionId: result.pendingTransactionId,
    approvalCaseId: result.approvalCaseId,
  } as const;
}
```

Send `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.

<PermissionsCards basePath="/v3/server/guides/permissions" pages={["developer-portal", "json", "testing", "production"]} cols={2} horizontal />

<Info>
  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.
</Info>

## Go further

Start from a tested Guardrail, hold some requests for approval, or manage approvers and wallet bindings from your backend.

<PermissionsCards basePath="/v3/server/guides/permissions" pages={["templates", "conditional-approvals", "backend"]} cols={3} />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.