Policies
Programmable policies codify guardrails for sensitive actions. Each policy holds rules, written as expression conditions, that are evaluated automatically when a withdrawal is requested or a smart contract is called. A matching rule can block the action, let it bypass the approval gate, or leave it to your normal approval flow.
At evaluation time, DENY wins: any matching DENY blocks the action. A matching ALLOW with no DENY bypasses the approval gate. No match falls through to the normal approval flow. Evaluation is fail-closed: a compile error, timeout, or infrastructure failure denies the action.
Like withdrawals, every policy and rule change goes through manual approval
before it takes effect. Each policy is tied to an approval group, so a
mutating endpoint (create, update, delete) does not apply the change directly:
it returns status: "pending_approval" with an approval_request_id, and the
change waits on the policy's approval group to sign off.
The policy model
A policy is the parent container for a set of rules. It is always gated behind an approval group, and can optionally be scoped to specific wallets.
- Name
- id
- Type
- string
- Description
The UUID of the policy.
- Name
- name
- Type
- string
- Description
Policy name, 3 to 100 characters.
- Name
- status
- Type
- string
- Description
One of
pending(awaiting approval),active(live),inactive(disabled), orrejected.
- Name
- version
- Type
- integer
- Description
Version number, incremented on each applied change.
- Name
- workspace_id
- Type
- string
- Description
The UUID of the owning workspace.
- Name
- approval_group_id
- Type
- string
- Description
The approval group UUID that gates changes to this policy. Every change is gated behind an approval request.
- Name
- wallet_ids
- Type
- array
- Description
Wallet UUIDs this policy targets. Empty means all wallets in the workspace.
- Name
- rules
- Type
- array
- Description
The rules belonging to this policy. See the rule model below.
The rule model
- Name
- id
- Type
- string
- Description
The UUID of the rule.
- Name
- trigger_action
- Type
- string
- Description
The action this rule evaluates:
withdrawal.createorweb3.contract_call. Immutable once created.
- Name
- condition
- Type
- string
- Description
The expression evaluated against the action payload, 1 to 4000 characters. See Writing conditions.
- Name
- effect
- Type
- string
- Description
DENY(block, always wins) orALLOW(bypass the approval gate).
- Name
- is_enabled
- Type
- boolean
- Description
Whether the rule participates in evaluation. Defaults to
true.
Get the policy schema
Return the fields a condition can reference, grouped per trigger action. The response is keyed by trigger action, each exposing sections (principal, resource, withdrawal, wallet, workspace, context) whose fields carry a name, type, and optional enum.
Request
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=="
Response
{
"success": true,
"message": "success",
"code": 0,
"data": {
"withdrawal.create": {
"principal": [
{ "name": "user_email", "type": "string" },
{ "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer", "guest"] }
],
"withdrawal": [
{ "name": "amount", "type": "number" },
{ "name": "value_usd", "type": "number" },
{ "name": "asset_symbol", "type": "string" },
{ "name": "is_whitelisted", "type": "boolean" },
{ "name": "destination_address", "type": "string" },
{ "name": "network_code", "type": "string" }
],
"wallet": [{ "name": "outflow_24h", "type": "number" }],
"workspace": [{ "name": "outflow_24h", "type": "number" }],
"context": [
{ "name": "hour", "type": "integer" },
{ "name": "day_of_week", "type": "string" }
]
},
"web3.contract_call": {
"principal": [
{ "name": "user_email", "type": "string" },
{ "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer", "guest"] }
],
"resource": [
{ "name": "network_code", "type": "string" },
{ "name": "contract_address", "type": "string" },
{ "name": "method_name", "type": "string" },
{ "name": "value_wei", "type": "string" },
{ "name": "decoded_args", "type": "map" }
],
"context": [
{ "name": "hour", "type": "integer" },
{ "name": "day_of_week", "type": "string" }
]
}
}
}
Create a policy
Create a policy in a workspace. Rules can be included inline or added later.
Body parameters
- Name
- name
- Type
- string
- Description
Required. 3 to 100 characters.
- Name
- description
- Type
- string
- Description
Optional, up to 500 characters.
- Name
- approval_group_id
- Type
- string
- Description
Required. The approval group UUID that must sign off on changes to this policy.
- Name
- wallet_ids
- Type
- array
- Description
Optional wallet UUIDs to target, up to 500. Empty applies to all wallets.
- Name
- rules
- Type
- array
- Description
Optional rules to create with the policy, up to 50. Each entry takes
trigger_action,condition,effect, and optionaldescriptionandis_enabled.
Request
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 && !withdrawal.is_whitelisted",
"effect": "DENY"
}
]
}'
Response
{
"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,
"approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"wallet_ids": [],
"rules": []
}
}
}
List policies
Retrieve all policies in a workspace, with their rules.
Query parameters
- Name
- status
- Type
- string
- Description
Optional filter:
pending,rejected,active, orinactive.
- Name
- offset
- Type
- integer
- Description
Optional. Records to skip.
- Name
- limit
- Type
- integer
- Description
Optional. Maximum records to return.
Request
curl -G 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==" \
-d status=active
Response
{
"success": true,
"message": "success",
"code": 0,
"data": [
{
"id": "9c8b7a6d-5e4f-3210-9a8b-7c6d5e4f3210",
"name": "Large withdrawal guard",
"status": "active",
"version": 1,
"approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"wallet_ids": [],
"rules": [
{
"id": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"trigger_action": "withdrawal.create",
"condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted",
"effect": "DENY",
"is_enabled": true
}
]
}
]
}
Get a policy
Retrieve a single policy and its rules.
Request
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
Update a policy's metadata, wallet targeting, or rule set. All body fields are optional; only the fields you send change.
Body parameters
- Name
- name
- Type
- string
- Description
Optional. 3 to 100 characters.
- Name
- description
- Type
- string
- Description
Optional, up to 500 characters.
- Name
- approval_group_id
- Type
- string
- Description
Optional approval group UUID.
- Name
- wallet_ids
- Type
- array
- Description
Optional. Replaces the targeted wallet set, up to 500 UUIDs.
- Name
- rules
- Type
- array
- Description
Optional. Replaces the entire rule set. An empty array clears all rules; omit to leave rules untouched.
Request
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 guard for large outflows",
"wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"]
}'
Response
{
"success": true,
"message": "success",
"code": 0,
"data": {
"status": "pending_approval",
"approval_request_id": "b7e2a1c4-9d8f-4a3b-8c1d-2e3f4a5b6c7d"
}
}
Delete a policy
Delete a policy and its rules.
Request
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=="
Response
{
"success": true,
"message": "success",
"code": 0,
"data": { "status": "applied" }
}
Get policy history
Retrieve the revision history of a policy. Each revision records the action, approval status, changed_by, and field-level changes (each with target_type, field, old_value, new_value).
Request
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=="
Response
{
"success": true,
"message": "success",
"code": 0,
"data": [
{
"version": 2,
"action": "edit",
"status": "approved",
"change_summary": "Updated description and wallet targeting",
"changed_by": "075243c1-962e-4803-83da-d73c78b8b060",
"changes": [
{
"target_type": "policy",
"field": "description",
"old_value": { "value": "Deny large outflows" },
"new_value": { "value": "Updated guard for large outflows" }
}
]
}
]
}
Get policy wallets
Retrieve a policy's wallet targeting. An empty wallet_ids array means all wallets in the workspace.
Request
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
Replace the wallets a policy targets. Send an empty array to apply to all wallets.
Body parameters
- Name
- wallet_ids
- Type
- array
- Description
Wallet UUIDs to target, up to 500. Empty targets all wallets.
Request
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 rule
Add a rule to an existing policy.
Body parameters
- Name
- trigger_action
- Type
- string
- Description
Required.
withdrawal.createorweb3.contract_call.
- Name
- condition
- Type
- string
- Description
Required. 1 to 4000 characters.
- Name
- effect
- Type
- string
- Description
Required.
ALLOWorDENY.
- Name
- description
- Type
- string
- Description
Optional, up to 500 characters.
- Name
- is_enabled
- Type
- boolean
- Description
Optional. Defaults to
true.
Request
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": "withdrawal.create",
"condition": "principal.role == \"proposer\" && withdrawal.value_usd > 1000",
"effect": "DENY"
}'
Response
{
"success": true,
"message": "success",
"code": 0,
"data": {
"status": "applied",
"rule": {
"id": "2b3c4d5e-6f70-8192-a3b4-c5d6e7f80910",
"policy_id": "9c8b7a6d-5e4f-3210-9a8b-7c6d5e4f3210",
"trigger_action": "withdrawal.create",
"condition": "principal.role == \"proposer\" && withdrawal.value_usd > 1000",
"effect": "DENY",
"is_enabled": true
}
}
}
Update a rule
Update a rule's condition, effect, description, or is_enabled. The trigger action is immutable: delete and recreate to change it.
Body parameters
- Name
- condition
- Type
- string
- Description
Optional. 1 to 4000 characters.
- Name
- effect
- Type
- string
- Description
Optional.
ALLOWorDENY.
- Name
- description
- Type
- string
- Description
Optional, up to 500 characters.
- Name
- is_enabled
- Type
- boolean
- Description
Optional. Enable or disable the rule.
Request
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 '{ "is_enabled": false }'
Delete a rule
Delete a rule from its policy.
Request
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=="
Validate a rule
Dry-run a condition without saving. It is compiled against the schema for its trigger action; a compile error is returned on failure. Body takes trigger_action, condition, and effect.
Request
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 && !withdrawal.is_whitelisted",
"effect": "DENY"
}'
Response
{
"success": true,
"message": "success",
"code": 0,
"data": { "valid": true }
}
Writing conditions
Conditions are expressions that return a boolean. Reference fields by section and name (e.g. withdrawal.value_usd, principal.role), combine with &&, ||, !, and compare with ==, !=, >, >=, <, <=. Call Get the policy schema for the fields available per trigger action.
- Name
- Block large unwhitelisted transfers
- Type
- DENY
- Description
withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted
- Name
- Restrict a role
- Type
- DENY
- Description
principal.role == "proposer" && withdrawal.value_usd > 1000
- Name
- Guard decoded contract arguments
- Type
- DENY
- Description
has(resource.decoded_args.to) && resource.decoded_args.to == "0x000000000000000000000000000000000000dead"
ABI decoding can fail, so guard any access to resource.decoded_args with a
has(...) check before reading a key.