---
title: "Policies"
description: "Learn how Fystack programmable policies evaluate withdrawals, Web3 transfers, contract calls, and message signing, and how to create, validate, approve, and manage policy rules."
url: https://docs.fystack.io/policies
last_updated: 2026-10-15
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# Policies

Fystack policies are programmable guardrails for sensitive wallet actions. A policy targets a workspace, optionally narrows to specific wallets, and contains rules that evaluate an expression against a structured action payload.

Rules run for four actions:

- `withdrawal.create` (trigger_action): Evaluated before a withdrawal is created. A `DENY` blocks the request. A
  matching `ALLOW` bypasses transaction approval and lets the withdrawal move
  to signing.
- `web3.transfer` (trigger_action): Evaluated when a native-coin or ERC-20 transfer is sent through the Web3
  raw-transaction API. Uses the same `withdrawal.*` fields as
  `withdrawal.create`, so one condition reads the same on both triggers.
  Velocity fields are not available, because these transfers are not
  withdrawals. ERC-20 transfers are also evaluated as `web3.contract_call`.
- `web3.contract_call` (trigger_action): Evaluated before EVM contract calls and ERC-20 token transfers are sent to
  signing. A `DENY` records the activity as `denied_by_policy`. A matching
  `ALLOW` skips transaction approval and proceeds to signing.
- `web3.sign_message` (trigger_action): Evaluated when a wallet is asked to sign a message with `personal_sign`,
  `eth_sign`, or `eth_signTypedData_v4`. Deny only: a signed message always
  goes through approval.

> **Info.** The policy schema is generated from the backend action schema in Apex. Use
> [Get the policy schema](#get-the-policy-schema) when building UI builders or
> validating field availability dynamically. For ready-made rules, see
> [Policy Templates](https://docs.fystack.io/product/policies.md#policy-templates).

---

## Policy structure

A policy is the container. A rule is the executable unit.

```json
{
  "name": "Large withdrawal guard",
  "approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
  "wallet_ids": [],
  "rules": [
    {
      "trigger_action": "withdrawal.create",
      "condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted",
      "effect": "DENY"
    }
  ]
}
```

- `approval_group_id` (string): The approval group that reviews policy creation, policy edits, policy
  deletion, and rule changes.
- `wallet_ids` (string\[]): Wallets this policy applies to. An empty array means every wallet in the
  workspace.
- `rules` (array): Up to 50 rules. Each rule has a `trigger_action`, `condition`, `effect`,
  optional `description`, and optional `is_enabled`.

### Policy statuses

- `pending` (status): Created but not approved yet. Pending policies are not evaluated.
- `active` (status): Approved and live. Only active policies are loaded by the evaluator.
- `inactive` (status): Disabled and not evaluated.
- `rejected` (status): Rejected during approval and not evaluated.

---

## Policy evaluation

For each guarded request, Fystack loads active policies for the workspace, compiles enabled rules, filters matches by action and wallet targeting, then resolves the result:

1. If any matching rule has `effect: "DENY"`, the action is blocked.
2. Otherwise, if any matching rule has `effect: "ALLOW"`, the action proceeds and transaction approval is bypassed.
3. Otherwise, no policy rule matched. The action continues through the normal approval flow.

`DENY` always wins over `ALLOW`, so rule order never changes the outcome. Within the winning effect, the first match in declaration order is used for attribution, so the API can surface a stable `rule_id` and policy reason.

There is no separate "require approval" effect. Approval is the default for any action that no `ALLOW` rule matches.

> **Warning.** Evaluation is fail-closed. Cache, database, compile, or runtime evaluation
> failures deny the action. Each evaluation has a 100 ms timeout.

### Scope and caching

Policies are scoped by workspace. Apex caches a compiled policy bundle per workspace and invalidates it after policy approvals, rejections, and wallet targeting changes. Wallet filtering happens during evaluation, so one workspace bundle can serve all wallets in that workspace.

---

## Conditions

`condition` is an expression that must return a boolean. Reference fields by section and field name, for example `principal.role`, `withdrawal.value_usd`, `wallet.purpose`, or `resource.method_selector`.

Use normal boolean, comparison, and membership operators:

```txt
&&  ||  !  ==  !=  >  >=  <  <=  in
```

`in` checks membership in a list literal or a list field, for example `principal.api_key_id in ["<api_key_id>"]` or `"0x…" in resource.permit_tokens`.

Examples:

- `Deny large unwhitelisted withdrawals` (DENY): `withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted`
- `Cap each wallet at $100,000 per day` (DENY): `wallet.projected_outflow_24h > 100000`
- `Keep custody funds inside the workspace` (DENY): `wallet.purpose == "custody" && !withdrawal.destination_is_internal`
- `Bypass approval for a payout API key` (ALLOW): `principal.initiator_type == "api_key" && principal.api_key_id in     ["<api_key_id>"] && withdrawal.value_usd < 1000`
- `Bypass approval for aged address book entries` (ALLOW): `withdrawal.is_whitelisted && withdrawal.address_age_sec >= 86400 &&
      withdrawal.value_usd <= 1000`
- `Deny weekend high-value withdrawals` (DENY): `(context.day_of_week == "Saturday" || context.day_of_week == "Sunday") &&
      withdrawal.value_usd > 25000`
- `Deny high-gas ERC-20 transfers` (DENY): `resource.method_selector == "0xa9059cbb" && resource.gas_limit > 100000`
- `Deny unlimited token permits` (DENY): `resource.is_permit && resource.permit_amount_is_unlimited`

> **Warning.** `resource.decoded_args` is dynamic. Guard dynamic fields with `has(...)`
> before reading them, for example `has(resource.decoded_args.to) &&
>   resource.decoded_args.to == "0x000000000000000000000000000000000000dead"`.

### Reference fields

Fields marked with a `ref` in the schema hold the ID or code of another workspace entity: `workspace_user`, `api_key`, `wallet`, `address_book`, `network`, or `asset_symbol`. Condition builders should render a picker for these fields.

When a rule is saved, Fystack checks that each referenced ID belongs to your workspace. If an entity is later deleted or revoked, the rule keeps the ID and can only match less, never more.

---

## Action schema

The action schema defines the fields available inside a condition. Fields marked `deprecated` are still evaluated for existing rules but should not be used in new ones.

### Common fields

Every trigger action exposes the following fields.

- `principal.user_id` (string · ref workspace_user): ID of the workspace member who initiated the action. Empty when the
  initiator is an API key.
- `principal.role` (string): Workspace role. One of `owner`, `admin`, `signer`, `proposer`, or `viewer`.
- `principal.initiator_type` (string): `user` for a web session, or `api_key` for an API key.
- `principal.api_key_id` (string · ref api_key): ID of the API key that initiated the action. Empty for web sessions, so
  combine it with `principal.initiator_type`.
- `principal.user_email` (string · deprecated): Email of the initiating user. Use `principal.user_id` instead, because an
  email can change.
- `wallet.id` (string · ref wallet): Source wallet ID. Use it for rules that target specific wallets inside one
  policy.
- `wallet.purpose` (string): Source wallet purpose. One of `general`, `custody`, `payment`, `user`,
  `one_time_use`, `gas_tank`, or `deployment`.
- `wallet.type` (string): Source wallet type. `standard` or `mpc`.
- `context.hour` (integer): UTC hour of evaluation, `0` through `23`.
- `context.day_of_week` (string): UTC weekday name, for example `Monday` or `Saturday`.

### Withdrawal fields

Available on `withdrawal.create` and `web3.transfer`.

- `withdrawal.amount` (number): Requested asset amount.
- `withdrawal.value_usd` (number): Requested value in USD.
- `withdrawal.asset_symbol` (string · ref asset_symbol): Asset symbol, such as `USDT`, `ETH`, or `BTC`.
- `withdrawal.asset_is_native` (boolean): Whether the asset is the network native asset.
- `withdrawal.asset_is_known` (boolean): Whether the token is in the Fystack asset catalog. When `false`, `amount`,
  `value_usd`, and `asset_symbol` are empty, so deny unknown tokens
  explicitly if you need to.
- `withdrawal.token_address` (string): Token contract address. Empty for native coins.
- `withdrawal.destination_address` (string): Recipient address.
- `withdrawal.network_code` (string · ref network): Internal network code.
- `withdrawal.is_whitelisted` (boolean): Whether the destination address is in any workspace address book entry.
- `withdrawal.address_book_id` (string · ref address_book): The address book entry that contains the destination address. One entry
  covers all of its addresses, so an entry works as a named list.
- `withdrawal.address_age_sec` (number): Seconds since the destination address was added to its address book entry.
  Use this for cooling-off rules on newly added recipients.
- `withdrawal.destination_is_internal` (boolean): Whether the destination is another wallet in the same workspace.
- `withdrawal.destination_wallet_id` (string · ref wallet): The destination wallet when the destination is internal.

### Withdrawal velocity fields

Available on `withdrawal.create` only. All values are in USD unless noted.

- `wallet.outflow_1h / outflow_6h / outflow_24h` (number): Realized outflow for the source wallet over the last 1, 6, or 24 hours.
- `wallet.withdrawal_count_1h` (integer): Withdrawal attempts from the source wallet in the last hour.
- `wallet.pending_outflow` (number): Value of withdrawals from the source wallet that are not executed yet.
- `wallet.projected_outflow_24h` (number): Realized 24-hour outflow, plus pending outflow, plus the withdrawal being
  checked, for the source wallet.
- `wallet.asset_projected_outflow_24h` (number): The same projection for the source wallet in units of the withdrawn asset,
  for example at most 5 BTC per day.
- `workspace.outflow_1h / outflow_6h / outflow_24h` (number): Realized outflow for the workspace over the last 1, 6, or 24 hours.
- `workspace.withdrawal_count_1h` (integer): Workspace withdrawal attempts in the last hour.
- `workspace.pending_outflow` (number): Value of workspace withdrawals that are not executed yet.
- `workspace.projected_outflow_24h` (number): Workspace projection: realized 24-hour outflow, plus pending outflow, plus
  the withdrawal being checked.
- `workspace.network_outflow_1h / network_outflow_6h / network_outflow_24h` (number): Realized workspace outflow on the current network over the last 1, 6, or
  24 hours.
- `workspace.network_withdrawal_count_1h` (integer): Workspace withdrawal attempts on the current network in the last hour.
- `workspace.network_pending_outflow` (number): Pending workspace outflow on the current network.
- `workspace.network_projected_outflow_24h` (number): Workspace projection on the current network.
- `workspace.initiator_projected_outflow_24h` (number): Projection for withdrawals created by the same member or API key, across
  the workspace.

Outflow and count fields are realized: they count a withdrawal only after it reaches an executed or successful terminal state. Pending outflow counts withdrawals that are waiting for approval, queued, or funding gas, whatever their age.

> **Info.** When an active rule uses a pending or projected field, Fystack evaluates the
> workspace's withdrawals one at a time, so two concurrent requests cannot both
> pass a limit. A request that waits longer than 5 seconds for its turn is
> rejected with HTTP `429` and can be retried.

### web3.contract_call fields

- `resource.network_code` (string · ref network): Internal network code.
- `resource.contract_address` (string): Lowercase contract address from the raw transaction `to` field.
- `resource.method_selector` (string): Lowercase 4-byte selector, for example `0xa9059cbb`.
- `resource.method_name` (string): Method name from request metadata when provided.
- `resource.value_wei` (string): Native value sent with the call, represented as a string.
- `resource.gas_limit` (integer): Gas limit from the raw transaction.
- `resource.decoded_args` (map): Decoded arguments when Fystack recognizes the call. ERC-20 `transfer`
  exposes `to` and `value`.

### web3.sign_message fields

- `resource.method` (string): Signing method. One of `personal_sign`, `eth_sign`, or
  `eth_signTypedData_v4`.
- `resource.network_code` (string · ref network): Internal network code.
- `resource.typed_data_primary_type` (string): EIP-712 primary type, for typed-data messages.
- `resource.typed_data_domain_name` (string): EIP-712 domain name, for typed-data messages.
- `resource.typed_data_verifying_contract` (string): EIP-712 verifying contract, for typed-data messages.
- `resource.is_permit` (boolean): `true` for EIP-2612, DAI, and Permit2 messages, which let the spender move
  tokens later without another signature.
- `resource.permit_spender` (string): The address the permit authorizes.
- `resource.permit_tokens` (string\[]): Token contracts the permit covers. Use it with `in`, for example
  `"0x…" in resource.permit_tokens`.
- `resource.permit_amount_is_unlimited` (boolean): `true` for max-value allowances (2^159 or more, or a DAI permit
  with `allowed` set).
- `resource.permit_deadline` (integer): Unix time the permit or its signature expires. `0` when the message has
  none.

---

## Approval lifecycle

Create, update, and delete policy operations are approval-gated. The mutation creates a `policy` approval request and returns `status: "pending_approval"` with an `approval_request_id`. The policy change is applied only after the approval group reaches threshold.

Rule create, update, and delete operations follow the same model when the parent policy has an approval group. The pending revision stores field-level diffs for audit and review.

Updating wallet targeting through `/wallets` applies immediately after validation and cache invalidation. Use the policy update endpoint when you want wallet targeting changes reviewed as part of a larger policy revision.

---

## Authentication

Policy API endpoints accept either a signed API-key request or a bearer access token.

- `ACCESS-API-KEY` (header): API key UUID for HMAC-authenticated requests.
- `ACCESS-TIMESTAMP` (header): UNIX timestamp in seconds used to build the HMAC signature.
- `ACCESS-SIGN` (header): Base64-encoded HMAC signature. See [Authentication](https://docs.fystack.io/authentication.md) for the signing format.
- `Authorization` (header): Bearer access token for user-session requests.

> **Warning.** Policy approval decision endpoints remain user-session endpoints. Use bearer access tokens for approval request approve and reject actions.

## Response envelope

Successful JSON responses use the standard Fystack envelope.

- `success` (boolean): `true` when the request succeeds.
- `message` (string): Response message, usually `success`.
- `code` (integer): Numeric result code. Success responses use `0`.
- `data` (object | array | null): Endpoint-specific response payload.

Validation errors use the same envelope with `success: false` and an `errors` array.

## Data types

### Policy

- `id` (string): Policy UUID.
- `created_at` (string): ISO timestamp when the policy was created.
- `updated_at` (string): ISO timestamp when the policy was last updated.
- `name` (string): Policy name. 3 to 100 characters.
- `description` (string): Optional description. Up to 500 characters.
- `status` (string): One of `pending`, `rejected`, `active`, or `inactive`.
- `version` (integer): Policy version number.
- `workspace_id` (string): Workspace UUID.
- `approval_group_id` (string | null): Approval group UUID assigned to the policy.
- `wallet_ids` (string\[]): Target wallet UUIDs. Empty array means the policy targets all wallets in the workspace.
- `rules` (PolicyRule\[]): Present on endpoints that preload rules.
- `approval_group` (object): Present on endpoints that preload the approval group.
- `pending_approval_request` (object): Present when there is a pending approval request for the policy.

### PolicyRule

- `id` (string): Rule UUID.
- `policy_id` (string): Parent policy UUID.
- `trigger_action` (string): One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`.
- `condition` (string): Boolean condition expression. 1 to 4000 characters.
- `effect` (string): One of `ALLOW` or `DENY`.
- `description` (string): Optional description. Up to 500 characters.
- `is_enabled` (boolean): Whether the rule is enabled. Defaults to `true` on create when omitted.

### Mutation response

Policy and rule mutation endpoints return a mutation payload in `data`.

- `status` (string): One of `applied`, `pending_approval`, or `deleted`, depending on the operation.
- `policy` (Policy): Returned by policy mutation endpoints when applicable.
- `rule` (PolicyRule): Returned by rule mutation endpoints when applicable.
- `approval_request_id` (string): Returned when the mutation is waiting for approval.

## Get the policy schema

**Endpoint:** `GET /policy/schema`

Returns fields a condition may reference, grouped by `trigger_action` and
section. Use this endpoint for condition builders and client-side previews.

Each field has a `name` and `type`. Optional keys:

- `enum` (string\[]): The allowed values for the field.
- `ref` (string): The entity the field refers to. Render a picker for it. See
  [Reference fields](#reference-fields).
- `deprecated` (boolean): Still evaluated for existing rules. Hide it from new-rule builders.

**Request** `GET /policy/schema`

```bash
curl -X GET https://api.fystack.io/api/v1/policy/schema \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

```json
{
  "success": true,
  "message": "success",
  "code": 0,
  "data": {
    "withdrawal.create": {
      "principal": [
        { "name": "user_email", "type": "string", "deprecated": true },
        { "name": "user_id", "type": "string", "ref": "workspace_user" },
        { "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer"] },
        { "name": "initiator_type", "type": "string", "enum": ["user", "api_key"] },
        { "name": "api_key_id", "type": "string", "ref": "api_key" }
      ],
      "withdrawal": [
        { "name": "amount", "type": "number" },
        { "name": "value_usd", "type": "number" },
        { "name": "asset_symbol", "type": "string", "ref": "asset_symbol" },
        { "name": "asset_is_native", "type": "boolean" },
        { "name": "asset_is_known", "type": "boolean" },
        { "name": "token_address", "type": "string" },
        { "name": "destination_address", "type": "string" },
        { "name": "network_code", "type": "string", "ref": "network" },
        { "name": "is_whitelisted", "type": "boolean" },
        { "name": "address_age_sec", "type": "number" },
        { "name": "address_book_id", "type": "string", "ref": "address_book" },
        { "name": "destination_is_internal", "type": "boolean" },
        { "name": "destination_wallet_id", "type": "string", "ref": "wallet" }
      ],
      "wallet": [
        { "name": "id", "type": "string", "ref": "wallet" },
        { "name": "purpose", "type": "string", "enum": ["general", "custody", "payment", "user", "one_time_use", "gas_tank", "deployment"] },
        { "name": "type", "type": "string", "enum": ["standard", "mpc"] },
        { "name": "outflow_1h", "type": "number" },
        { "name": "outflow_6h", "type": "number" },
        { "name": "outflow_24h", "type": "number" },
        { "name": "withdrawal_count_1h", "type": "integer" },
        { "name": "pending_outflow", "type": "number" },
        { "name": "projected_outflow_24h", "type": "number" },
        { "name": "asset_projected_outflow_24h", "type": "number" }
      ],
      "workspace": [
        { "name": "outflow_1h", "type": "number" },
        { "name": "outflow_6h", "type": "number" },
        { "name": "outflow_24h", "type": "number" },
        { "name": "withdrawal_count_1h", "type": "integer" },
        { "name": "network_outflow_1h", "type": "number" },
        { "name": "network_outflow_6h", "type": "number" },
        { "name": "network_outflow_24h", "type": "number" },
        { "name": "network_withdrawal_count_1h", "type": "integer" },
        { "name": "pending_outflow", "type": "number" },
        { "name": "projected_outflow_24h", "type": "number" },
        { "name": "network_pending_outflow", "type": "number" },
        { "name": "network_projected_outflow_24h", "type": "number" },
        { "name": "initiator_projected_outflow_24h", "type": "number" }
      ],
      "context": [
        { "name": "hour", "type": "integer" },
        { "name": "day_of_week", "type": "string" }
      ]
    },
    "web3.transfer": {
      "principal": [ "... same as withdrawal.create ..." ],
      "withdrawal": [ "... same as withdrawal.create ..." ],
      "wallet": [
        { "name": "id", "type": "string", "ref": "wallet" },
        { "name": "purpose", "type": "string", "enum": ["general", "custody", "payment", "user", "one_time_use", "gas_tank", "deployment"] },
        { "name": "type", "type": "string", "enum": ["standard", "mpc"] }
      ],
      "context": [ "... same as withdrawal.create ..." ]
    },
    "web3.contract_call": {
      "principal": [ "... same as withdrawal.create ..." ],
      "resource": [
        { "name": "network_code", "type": "string", "ref": "network" },
        { "name": "contract_address", "type": "string" },
        { "name": "method_selector", "type": "string" },
        { "name": "method_name", "type": "string" },
        { "name": "value_wei", "type": "string" },
        { "name": "gas_limit", "type": "integer" },
        { "name": "decoded_args", "type": "map" }
      ],
      "wallet": [ "... id, purpose, type ..." ],
      "context": [ "... same as withdrawal.create ..." ]
    },
    "web3.sign_message": {
      "principal": [ "... same as withdrawal.create ..." ],
      "resource": [
        { "name": "method", "type": "string", "enum": ["personal_sign", "eth_sign", "eth_signTypedData_v4"] },
        { "name": "network_code", "type": "string", "ref": "network" },
        { "name": "typed_data_primary_type", "type": "string" },
        { "name": "typed_data_domain_name", "type": "string" },
        { "name": "typed_data_verifying_contract", "type": "string" },
        { "name": "is_permit", "type": "boolean" },
        { "name": "permit_spender", "type": "string" },
        { "name": "permit_tokens", "type": "string[]" },
        { "name": "permit_amount_is_unlimited", "type": "boolean" },
        { "name": "permit_deadline", "type": "integer" }
      ],
      "wallet": [ "... id, purpose, type ..." ],
      "context": [ "... same as withdrawal.create ..." ]
    }
  }
}
```

## Validate a rule

**Endpoint:** `POST /workspaces/{workspace_id}/policies/rules/validate`

Validates a rule body without creating or updating a rule.

### Path parameters

- `workspace_id` (string): Workspace UUID.

### Body parameters

- `trigger_action` (string, required): One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`.
- `condition` (string, required): 1 to 4000 characters. Must compile to a boolean expression.
- `effect` (string, required): One of `ALLOW` or `DENY`. `web3.sign_message` rules are deny only.

### Response data

- `valid` (boolean): `true` when the rule body validates.

**Request** `POST /workspaces/{workspace_id}/policies/rules/validate`

```bash
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/validate \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_action": "withdrawal.create",
    "condition": "withdrawal.value_usd > 10000",
    "effect": "DENY"
  }'
```

```json
{
  "success": true,
  "message": "success",
  "code": 0,
  "data": { "valid": true }
}
```

## Create a policy

**Endpoint:** `POST /workspaces/{workspace_id}/policies`

Creates a policy mutation.

### Path parameters

- `workspace_id` (string): Workspace UUID.

### Body parameters

- `name` (string, required): 3 to 100 characters.
- `description` (string): Optional. Up to 500 characters.
- `approval_group_id` (string, required): Approval group UUID.
- `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs.
- `rules` (PolicyRuleCreate\[]): Optional. Up to 50 rules.

**Request** `POST /workspaces/{workspace_id}/policies`

```bash
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Large withdrawal guard",
    "approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "wallet_ids": [],
    "rules": [
      {
        "trigger_action": "withdrawal.create",
        "condition": "withdrawal.value_usd > 10000",
        "effect": "DENY"
      }
    ]
  }'
```

```json
{
  "success": true,
  "message": "success",
  "code": 0,
  "data": {
    "status": "pending_approval",
    "approval_request_id": "b7e2a1c4-9d8f-4a3b-8c1d-2e3f4a5b6c7d",
    "policy": {
      "id": "9c8b7a6d-5e4f-3210-9a8b-7c6d5e4f3210",
      "name": "Large withdrawal guard",
      "status": "pending",
      "version": 1,
      "wallet_ids": []
    }
  }
}
```

## List policies

**Endpoint:** `GET /workspaces/{workspace_id}/policies`

Returns policies in a workspace.

### Path parameters

- `workspace_id` (string): Workspace UUID.

### Query parameters

- `status` (string): Optional. One of `pending`, `rejected`, `active`, or `inactive`.
- `limit` (integer): Optional. Maximum `1000`.
- `offset` (integer): Optional. Maximum `100000`.

### Response data

- `data` (Policy\[]): Array of policy objects.

**Request** `GET /workspaces/{workspace_id}/policies`

```bash
curl -X GET "https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies?status=active&limit=20&offset=0" \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Get a policy

**Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}`

Returns a single policy.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Response data

- `data` (Policy): Policy object, including rules, approval group, and pending approval request when present.

**Request** `GET /workspaces/{workspace_id}/policies/{policy_id}`

```bash
curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Update a policy

**Endpoint:** `PUT /workspaces/{workspace_id}/policies/{policy_id}`

Updates policy fields.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Body parameters

- `name` (string): Optional. 3 to 100 characters.
- `description` (string): Optional. Up to 500 characters.
- `approval_group_id` (string): Optional approval group UUID.
- `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs. Send an empty array to target all wallets.
- `rules` (PolicyRuleCreate\[]): Optional. Replaces the complete rule set when present. Send an empty array to clear all rules.

**Request** `PUT /workspaces/{workspace_id}/policies/{policy_id}`

```bash
curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Updated policy description",
    "wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"]
  }'
```

## Delete a policy

**Endpoint:** `DELETE /workspaces/{workspace_id}/policies/{policy_id}`

Deletes a policy or creates a delete mutation that waits for approval.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Response data

- `data` (Mutation response): Policy mutation response.

**Request** `DELETE /workspaces/{workspace_id}/policies/{policy_id}`

```bash
curl -X DELETE https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Get policy history

**Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}/history`

Returns policy revisions for one policy.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Response data

- `data` (PolicyRevision\[]): Array of policy revisions, including field-level `changes` when present.

**Request** `GET /workspaces/{workspace_id}/policies/{policy_id}/history`

```bash
curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/history \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Get policy wallets

**Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}/wallets`

Returns the policy with its current `wallet_ids`.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Response data

- `data` (Policy): Policy object.

**Request** `GET /workspaces/{workspace_id}/policies/{policy_id}/wallets`

```bash
curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/wallets \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Update policy wallets

**Endpoint:** `PUT /workspaces/{workspace_id}/policies/{policy_id}/wallets`

Replaces a policy's wallet targeting list.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Body parameters

- `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs. Send an empty array to target all wallets.

**Request** `PUT /workspaces/{workspace_id}/policies/{policy_id}/wallets`

```bash
curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/wallets \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{ "wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"] }'
```

## Create a policy rule

**Endpoint:** `POST /workspaces/{workspace_id}/policies/{policy_id}/rules`

Creates one rule under an existing policy.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `policy_id` (string): Policy UUID.

### Body parameters

- `id` (string): Optional rule UUID.
- `trigger_action` (string, required): One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`.
- `condition` (string, required): 1 to 4000 characters.
- `effect` (string, required): One of `ALLOW` or `DENY`. `web3.sign_message` rules are deny only.
- `description` (string): Optional. Up to 500 characters.
- `is_enabled` (boolean): Optional. Defaults to `true`.

**Request** `POST /workspaces/{workspace_id}/policies/{policy_id}/rules`

```bash
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/rules \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_action": "web3.contract_call",
    "condition": "resource.gas_limit > 100000",
    "effect": "DENY",
    "is_enabled": true
  }'
```

## Update a policy rule

**Endpoint:** `PUT /workspaces/{workspace_id}/policies/rules/{rule_id}`

Updates mutable rule fields. The trigger action is immutable; delete and
recreate the rule to change it.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `rule_id` (string): Rule UUID.

### Body parameters

- `condition` (string): Optional. 1 to 4000 characters.
- `effect` (string): Optional. One of `ALLOW` or `DENY`.
- `description` (string): Optional. Up to 500 characters.
- `is_enabled` (boolean): Optional.

**Request** `PUT /workspaces/{workspace_id}/policies/rules/{rule_id}`

```bash
curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/{rule_id} \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature==" \
  -H "Content-Type: application/json" \
  -d '{
    "condition": "resource.gas_limit > 150000",
    "is_enabled": true
  }'
```

## Delete a policy rule

**Endpoint:** `DELETE /workspaces/{workspace_id}/policies/rules/{rule_id}`

Deletes one policy rule or creates a rule deletion mutation that waits for approval.

### Path parameters

- `workspace_id` (string): Workspace UUID.
- `rule_id` (string): Rule UUID.

### Response data

- `data` (Mutation response): Rule mutation response.

**Request** `DELETE /workspaces/{workspace_id}/policies/rules/{rule_id}`

```bash
curl -X DELETE https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/{rule_id} \
  -H "ACCESS-API-KEY: your_api_key" \
  -H "ACCESS-TIMESTAMP: 1667836889" \
  -H "ACCESS-SIGN: YourBase64EncodedSignature=="
```

## Design patterns

Use `DENY` rules for hard controls: sanctions lists, unknown recipients, new address book entries, projected outflow limits, unknown tokens, unlimited permits, or high-risk contract methods.

Use `ALLOW` rules narrowly. A matching `ALLOW` skips approval, so conditions should be precise: low USD value, trusted initiator, known wallet target, known network, and a known recipient with an address age check, or a known contract and method.

Pair every `ALLOW` with a `DENY` ceiling, such as `wallet.projected_outflow_24h` or `workspace.initiator_projected_outflow_24h`. Because `DENY` always wins, the ceiling also caps the auto-approved path.

Keep policy sets small and explicit. Broad workspace-wide policies are useful for baseline controls; wallet-scoped policies are better when a hot wallet, treasury wallet, or customer wallet has different risk thresholds.

See [Policy Templates](https://docs.fystack.io/product/policies.md#policy-templates) for complete examples.
