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

# Test your policies

> Test every Guardrail rule on a Development key and read each result before production review

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>;
};

Test with the pregenerated wallets your server signs with, using a Development API key.

Test every rule on a Development key before you ask Para to publish it to production. This page gives you a test plan and shows where to read each result.

<Info>
  Production review starts from the policy that is active on your Development key, so test the version you intend to ship. Changing beta later never updates production on its own.
</Info>

## Set up a test environment

Policies belong to one API key, and a Development key signs against Para's beta environment. A few choices up front make results easier to read:

* Use a Development key, and activate the policy on the same key your test app or backend uses. For REST, that is a beta secret key against `https://api.beta.getpara.com`.
* Sign on testnets. Sepolia, Solana Devnet, Stellar Testnet and Sui Testnet keep test funds separate from real ones.
* Prepare one account per role. Requests need a user who consents. Approval requirements need approver accounts that are separate from the account that starts the request, with roles assigned from your backend.

<Tip>
  A selector that names a whole wallet type, with no `chainId`, covers testnets and mainnets alike, so the rules you test are the rules you ship. Values that differ by network, such as token contracts, still need mainnet equivalents for production.
</Tip>

## Validate before you activate

Validation catches malformed policies before they reach signing. In the Developer Portal, **Review** validates the policy and lists overlapping active policies. With the CLI, run `para keys permissions validate`, which exits with code `10` when the policy is invalid.

A policy can pass validation and still not do what you meant. Read the normalized policy on the Review step, or in **Normalized policy JSON** after saving, and check the overlap list: every overlapping policy applies, and a block in any overlapping Guardrail wins.

<Note>
  When Para refuses a CLI request with HTTP 403, the CLI usually prints a generic `API_FORBIDDEN` message in place of Para's. Start by checking that your developer account can manage the organization, project and API key.
</Note>

## Build a test matrix

Write down the result you expect for each case before you send it. Most rows need one request that should pass and one that should not.

| Area | Send | Expect |
| - | - | - |
| Each condition | One request inside the boundary and one just outside it, such as the exact limit and one base unit above | `lte` and `gte` include the limit, `lt` and `gt` exclude it |
| Actions you did not name | A transaction type no rule covers | [Para's defaults](/v3/concepts/permissions#multiple-policies-and-paras-defaults): app-owned wallets sign and user-owned wallets go to user review, raw signatures included |
| Spend windows | Requests that add up to the limit, the one that crosses it, and one after the window resets | The crossing request no longer passes the rule; spend resets at the window boundary |
| Policy time window | Requests before **From**, inside the window and after **To** | The policy applies only inside the window |
| Request consent | One run where the user approves, one where they choose manual review | Standing access signs matching requests without a prompt; manual review opens review every time |
| Approval requirements | Approvals from separate approver accounts, a rejection, and an amount on each side of every stage threshold | The case completes only after each stage has its approvals |
| Facts Para cannot read | A request your rule cannot evaluate, such as a call to a contract Para cannot decode | Denied with a reason such as `missing-fact` |

<AccordionGroup>
  <Accordion title="Spend windows">
    Spend windows are fixed buckets aligned to the Unix epoch in UTC, not rolling windows. A `1d` or `24h` window resets at 00:00 UTC, and a one-week window resets at 00:00 UTC on Thursday.

    Para compares the spend already recorded in the bucket plus the current request against the limit. Spend counts only when the request is allowed to sign, so a denied request uses no budget, and spend from before the policy was active does not count. Budgets belong to the wallet, network, asset and window, so two policies that meter the same thing share one budget.
  </Accordion>

  <Accordion title="Request consent">
    A user who has not consented sees the consent screen in the Portal before the review of a matching transaction. **Approve** grants standing access to future matching requests, while the current request still goes through review. **Manually approve each transaction instead** keeps every matching request in review.

    Test both choices, and test **Revoke permission** on the standalone consent page. Consent belongs to one policy version, so after you save a new version, each user consents again.
  </Accordion>

  <Accordion title="Approval requirements">
    Use a separate account for each approver. The account that started a request cannot approve its case unless the requirement collects transaction signatures, and an approver counted in one stage cannot count again in a later stage.

    One rejection from an eligible approver ends the case as `rejected`. For amount bands, send an amount just below and exactly at each `minimumAmount`: a stage applies once the amount reaches its threshold, and higher amounts add stages rather than replacing them. A completed approval covers one request and is used once.
  </Accordion>

  <Accordion title="Facts Para cannot read">
    Some facts depend on decoding the request, such as a contract's method and arguments. When a rule reads a fact that is missing for a request, the rule cannot decide, and Para denies the request unless another matching rule with an equal or stronger effect decides it. Test with the exact requests your app produces.
  </Accordion>
</AccordionGroup>

## Read the results

Each signing call tells you which way the policy decided. Denials name the deciding rule when one matched, and requests that need approval return a case you can follow.

<Tabs>
  <Tab title="REST API">
    A denied request returns HTTP 403 with code `POLICY_DENIED`. Its `deniedBy` entry names the policy and rule that decided, when a rule matched, and a `reason`.

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

    A request that needs approval returns HTTP 403 with code `POLICY_REVIEW_REQUIRED`, plus `approvalCaseId`, `approvalCaseStatus` and `reason`. Once the case completes, resend the identical request with the same `Idempotency-Key`.
  </Tab>

  <Tab title="Web and React Native SDKs">
    On React Native, a denied request throws an error object with `code: 'POLICY_DENIED'`, `status: 403` and the same `deniedBy` entry under `data.deniedBy`. Check its `code` property, because the object is not an `Error` instance. The web and React SDKs currently throw a generic error with no `code`, so a web test sees a denial only as a failed signing call.

    A user-owned wallet request that needs review sends the user to the Portal review, where consent and manual approval happen.
  </Tab>
</Tabs>

The `reason` tells you why the request was denied:

| `reason` | Meaning |
| - | - |
| `matched:partner:<ruleId>` | Your `deny` rule matched |
| `default-deny` | No rule matched, and nothing else allowed the request |
| `missing-fact` | A rule needed a fact Para could not read for this request |
| `review-rejected` | An approver rejected the request |
| `windowed-spend-limit-exceeded` | The request would cross a spend window limit |

<Note>
  A denial from an older scope-based policy can omit `deniedBy`.
</Note>

To follow an approval, read its case. `status` is `pending`, `completed`, `rejected`, `expired`, `invalidated` or `cancelled`, and an ended case carries a `terminalReason`. On SDKs, an approver reads the case with `getAccessibleApprovalCase` and the requester with `getInitiatedApprovalCase`. Your backend reads it with `getPartnerApprovalCase`.

To check spend windows, read the policy's usage meters. Each meter reports `limitBaseUnits`, `usedBaseUnits` and `resetAt`. An `allow` limit written with `lt` or `lte` also reports `remainingBaseUnits`.

<CodeGroup>
  ```ts REST SDK theme={null}
  const usage = await para.getPartnerPolicyUsage("POLICY_ID", {
    walletId: "WALLET_ID",
    chainId: "11155111",
  });
  ```

  ```ts Web and React Native SDKs theme={null}
  const usage = await para.getUserPolicyUsage("POLICY_ID", {
    walletId: "WALLET_ID",
    chainId: "11155111",
  });
  ```
</CodeGroup>

<CardGroup cols={1}>
  <Card imgUrl="/images/v3/cards/concept-error-handling-horizontal.png" title="Permissions errors" description="Every Permissions error code and reason, with how to handle it." href="/v3/references/permissions/errors" horizontal />
</CardGroup>

## Fix and test again

Edit the policy, then rerun the rows it affects. Saving changes to an active policy replaces it with the next version, and consent given for the previous version does not carry over. If the change would invalidate open approval cases, the Developer Portal and the CLI apply it only after you confirm. Keep testing until every row in your matrix gives the result you wrote down.

<PermissionsCards basePath="/v3/server/guides/permissions" pages={["production"]} cols={1} horizontal />


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