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

# Approval thresholds and tiers

> Hold large transfers from app-owned wallets for approval, or sign, approve or deny them by amount.

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

These templates hold larger transfers from an app-owned wallet until the right people approve them, and can deny the largest outright. Approval requirements are JSON only, because the Developer Portal forms cannot express them.

<Info>
  Para turns on conditional approvals per API key, so your beta and production keys are enabled separately. Until it does, validating or saving a policy with an `approvalRequirement` fails with `permissions_v2_extension_unavailable`.
</Info>

## Set up approvers

An approval comes from a person signed in to Para, never from your server. Your backend decides who those people are before the first case opens.

<Steps>
  <Step title="Assign roles">
    Once the policy is active, give the roles it names, such as `approver`, to the Para users who will review requests. Your backend assigns roles inside an authorization scope, and Para accepts only roles that an active policy declares.
  </Step>

  <Step title="Bind the wallet">
    Bind the app-owned wallet to the same authorization scope, so its cases go to the people you assigned.
  </Step>

  <Step title="Review in Para">
    Approvers open each case in Para and approve or reject it.
  </Step>
</Steps>

The request comes from your backend, so Para does not know which person asked for it. If a team member asked for the transfer, pass their Para user ID as `initiatorUserId` on the signing request, and Para leaves them out of every stage. The user must be linked to your app, or the request fails with `user is not linked to this app`.

Once cases are open, a change to role assignments or to the wallet's binding that would invalidate them fails with `409 APPROVAL_CASES_AFFECTED` and lists the `affectedCaseIds`. Send the change again with `force: true` to apply it anyway.

## Finish or cancel a request

A request that needs approval opens an approval case and does not sign. Para rejects the signing request with `POLICY_REVIEW_REQUIRED`. The case ID is `approvalCaseId`, at the top level of the REST response body or in `error.data` from the server SDK. Every stage needs its approvals before the case is `completed`. One rejection from an eligible approver ends the case, and a case that is still open after 7 days expires.

When the case is `completed`, send the request again. Through REST, use the same `Idempotency-Key` as the first attempt. Through the server SDK, pass the case ID as `resumeApprovalCaseId`. Para still matches the case if you rebuilt the transaction with a new nonce or new gas values. A completed case covers one signature. The conditional approvals guide for your platform shows how to track the case.

To drop a request that is still waiting, cancel its case with `cancelPartnerApprovalCase(caseId)` in the REST SDK. The case ends as `cancelled` and the request never signs.

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

## Require approval above a threshold

Use this when routine transfers should flow and large ones need a second pair of eyes. ETH transfers on Base above 1 ETH wait for one person with the `approver` role.

```json guardrail-approval-threshold.json theme={null}
{
  "id": "base-large-transfer-approval",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM",
    "chainId": "8453"
  },
  "rules": [
    {
      "id": "large-transfer-approval",
      "effect": "require_approval",
      "actions": ["transfer_native"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "native_transfer" },
          { "kind": "compare", "fact": "request.value.baseUnits", "op": "gt", "value": "1000000000000000000" }
        ]
      },
      "approvalRequirement": {
        "id": "large-transfer-review",
        "evidence": "authenticated_approval",
        "stages": [
          { "id": "approvers", "requiredApprovals": 1, "eligibility": [{ "kind": "role", "name": "approver" }] }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Require one approver for ETH transfers above 1 ETH on Base"
  }
}
```

| Signing request | Result |
| - | - |
| Send 0.5 ETH | Signs |
| Send 2 ETH | Needs approval from one `approver` |

<Check>
  Test it on your Development key: assign `approver` to a test user and bind the wallet, then sign a 2 ETH transfer with an `Idempotency-Key`. Approve the case as that user, send the request again with the same key, and it signs.
</Check>

## Sign, approve or deny by amount

Use this to split transfers into three tiers: up to 1 ETH signs, more than 1 ETH and up to 10 ETH waits for an approver, and more than 10 ETH is denied.

```json guardrail-approval-tiers.json theme={null}
{
  "id": "base-transfer-tiers",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM",
    "chainId": "8453"
  },
  "rules": [
    {
      "id": "deny-above-10-eth",
      "effect": "deny",
      "actions": ["transfer_native"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "native_transfer" },
          { "kind": "compare", "fact": "request.value.baseUnits", "op": "gt", "value": "10000000000000000000" }
        ]
      }
    },
    {
      "id": "approve-above-1-eth",
      "effect": "require_approval",
      "actions": ["transfer_native"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "native_transfer" },
          { "kind": "compare", "fact": "request.value.baseUnits", "op": "gt", "value": "1000000000000000000" }
        ]
      },
      "approvalRequirement": {
        "id": "mid-size-transfer-review",
        "evidence": "authenticated_approval",
        "stages": [
          { "id": "approvers", "requiredApprovals": 1, "eligibility": [{ "kind": "role", "name": "approver" }] }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Sign ETH up to 1, approve up to 10, deny above 10 on Base"
  }
}
```

A 15 ETH transfer matches both rules. Para ranks `deny` above `require_approval`, so the transfer is denied without opening a case. The order of the rules in the policy does not change this.

| Signing request | Result |
| - | - |
| Send 0.5 ETH | Signs |
| Send 5 ETH | Needs approval from one `approver` |
| Send 15 ETH | Denied by `deny-above-10-eth` |

<Check>
  Test it on your Development key: sign 0.5 ETH, 5 ETH and 15 ETH transfers. Only the 5 ETH transfer opens a case.
</Check>

## Add approvers as the amount grows

Use this when larger transfers need more senior approval on top of the usual one. Every transfer above 1 ETH needs an `approver`, and transfers of 10 ETH or more also need a `finance-lead`.

```json guardrail-approval-amount-bands.json theme={null}
{
  "id": "base-transfer-approval-bands",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM",
    "chainId": "8453"
  },
  "rules": [
    {
      "id": "large-transfer-approval",
      "effect": "require_approval",
      "actions": ["transfer_native"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "native_transfer" },
          { "kind": "compare", "fact": "request.value.baseUnits", "op": "gt", "value": "1000000000000000000" }
        ]
      },
      "approvalRequirement": {
        "id": "large-transfer-review",
        "evidence": "authenticated_approval",
        "amountFact": "request.value.baseUnits",
        "stages": [
          { "id": "approver", "requiredApprovals": 1, "eligibility": [{ "kind": "role", "name": "approver" }] },
          {
            "id": "finance-lead",
            "requiredApprovals": 1,
            "eligibility": [{ "kind": "role", "name": "finance-lead" }],
            "minimumAmount": "10000000000000000000"
          }
        ],
        "safeKnobs": [
          {
            "id": "finance-lead-threshold",
            "kind": "minimum_amount",
            "stageId": "finance-lead",
            "min": "5000000000000000000",
            "max": "20000000000000000000"
          }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Add a finance lead approval for ETH transfers of 10 or more on Base"
  }
}
```

`amountFact` names the amount Para compares with each stage's `minimumAmount`. The first stage has no minimum and applies to every transfer the rule matches. A later stage joins when the amount is at least its minimum, so higher amounts add stages instead of replacing them. Stages run in order.

The `safeKnobs` entry lets your backend, or a configuration manager you name, move the `finance-lead` threshold for an authorization scope without publishing a new policy version. `min` and `max` bound the value, so here it can sit anywhere from 5 ETH to 20 ETH. The published 10 ETH must fall inside that range, and saving a value outside it fails with `safe_knob_value_invalid`. Without `max`, the threshold could only be lowered.

| Signing request | Result |
| - | - |
| Send 0.5 ETH | Signs |
| Send 5 ETH | Needs approval from one `approver` |
| Send 10 ETH or more | Needs approval from one `approver`, then one `finance-lead` |

<Warning>
  A person counts once per case. Someone who approved the first stage cannot also approve the second, even if they hold both roles, so assign `finance-lead` to someone other than your approvers.
</Warning>

<Check>
  Test it on your Development key: sign a 10 ETH transfer and check that the case moves to the `finance-lead` stage only after the `approver` stage completes.
</Check>

## Publish your tiers

On beta, roles go to test users. Production is a separate environment, so once the tiers behave as expected, request a production review and set up the production roles and wallet binding there too.

<CardGroup cols={1}>
  <Card title="Approval requirements reference" description="Every field of an approval requirement, with stage, band and eligibility rules." href="/v3/references/permissions/approval-requirements" imgUrl="/images/v3/cards/concept-approval-requirements-horizontal.png" horizontal />
</CardGroup>

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

<PermissionsCards basePath="/v3/server/guides/permissions" pages={["templates/treasury", "backend"]} cols={2} />


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