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

> ## Agent Instructions
> Bitnob API requests authenticate with an `X-Auth-Client` API key header against https://api.bitnob.com.
> Always use the sandbox environment for testing. Never put live API keys in sample code.
> Money amounts are integers in the smallest unit of the currency (satoshis for BTC, cents for fiat). Never use floats.
> Every state-changing request takes a client-supplied `reference` and must be treated as idempotent on retry.
> Quotes expire. Re-quote rather than reusing a stale quote id.

# Write and activate your first Enterprise policy

> Draft a two-rule policy, lint it, dry-run it against sample transfers, and send it to quorum for activation.

By the end of this guide your organization will enforce a policy with two rules:

1. Block any transfer worth more than \$50,000.
2. Require one approval for any transfer worth more than \$10,000.

Anything at or below \$10,000 matches neither rule. You will test that case too.

## Before you start

* A sandbox API key with permission to manage policies, exported as `PLATFORM_API_KEY`. See [Set up your account](https://www.bitnob.dev/docs/platform/account-setup.md).
* A quorum policy with at least two approvers, configured in the Bitnob dashboard. Without one, the last step returns `422`. See [Activation always goes through quorum](https://www.bitnob.dev/docs/platform/policies/policy-lifecycle-and-quorum.md#activation-always-goes-through-quorum).

**1. Pick an approver**

[List approver candidates](https://www.bitnob.dev/api-reference/platform/users/list-approver-candidates.md) returns the users and roles a `require_approval` rule can name.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/users/approver-candidates \
  -H "X-API-Key: $PLATFORM_API_KEY"
```

```json theme={null}
{
  "object": "list",
  "data": [
    { "object": "approver_candidate", "type": "role", "id": "owner", "name": "Owner", "member_count": 1 },
    { "object": "approver_candidate", "type": "role", "id": "admin", "name": "Manager", "member_count": 3 },
    { "object": "approver_candidate", "type": "role", "id": "treasury_officer", "name": "Treasury Officer", "member_count": 2 },
    { "object": "approver_candidate", "type": "user", "id": "3f1a2b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "name": "Jane Doe", "org_role": "operator" }
  ],
  "has_more": false
}
```

Use a role where you can. It keeps working when people join or leave. This guide uses the `admin` role, which the dashboard labels Manager. Use the `id`, not the display name. Check `member_count`: a role with no members can never approve.

**2. Lint the rules before you save them**

[Validate in-flight rule array](https://www.bitnob.dev/api-reference/platform/policies/validate-in-flight-rule-array.md) checks a rule list without saving anything. Put the `block` rule first: rules run in order and the first match wins, so a transfer of \$60,000 must hit the block before it hits the approval.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/policies/validate \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Transfer limits",
    "rules": [
      {
        "name": "Block over $50,000",
        "order": 1,
        "action_type": "block",
        "conditions": [
          { "field": "amount_usd", "operator": "greater_than", "value": "50000" }
        ]
      },
      {
        "name": "Approve over $10,000",
        "order": 2,
        "action_type": "require_approval",
        "conditions": [
          { "field": "amount_usd", "operator": "greater_than", "value": "10000" }
        ],
        "action_config": {
          "required_approvals": 1,
          "approvers": [{ "id": "admin", "type": "role" }]
        }
      }
    ]
  }'
```

```json theme={null}
{
  "valid": true,
  "issue_count": { "error": 0, "warning": 0, "info": 0 },
  "diagnostics": []
}
```

> [!NOTE]
>
> The keys inside `action_config` depend on the action type. A `require_approval` rule needs both `approvers` and `required_approvals`. This endpoint only checks how rules interact. The field, operator, and `action_config` checks happen when you save, in the next step.

Try swapping the two `order` values and validating again. The block rule now comes second and can never match, because every transfer over \$50,000 is also over \$10,000. Validation returns a warning with the code `shadowed_by_different_action`, and a `suggested_fix` telling you to move the block rule up.

**3. Create the draft**

[Create draft policy](https://www.bitnob.dev/api-reference/platform/policies/create-draft-policy.md) saves the same body as a draft. It requires an `Idempotency-Key`, so a retry after a timeout doesn't create a second draft.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/policies \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: policy-transfer-limits-v1" \
  -d '{
    "name": "Transfer limits",
    "category": "limits",
    "description": "Block over $50k, approve over $10k",
    "rules": [ ...the two rules from step 3... ]
  }'
```

The response is the policy, with `"status": "draft"` and an `id`. Save the `id` as `POLICY_ID`. A draft is not enforced, so nothing changes for your transactions yet.

Saving checks every rule as you save it. An unknown condition field returns `400 invalid_condition_field`, an unsupported operator `invalid_operator`, and a missing or unrecognized `action_config` key `invalid_action_config` or `unknown_action_config_field`. Numeric condition values come back as numbers, for example `"value": 50000`.

**4. Dry-run three transfers**

[Dry-run policy evaluation](https://www.bitnob.dev/api-reference/platform/policies/dry-run-policy-evaluation.md) runs a sample request through a policy and returns the decision. Pass `policy_id` to evaluate the draft. Without it, the engine uses the enforced policy. Nothing is created or logged.

```bash theme={null}
curl https://api.platform.bitnob.com/v1/policies/evaluate \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "policy_id": "'"$POLICY_ID"'",
    "chain": "tron",
    "asset_symbol": "USDT",
    "amount_usd": "25000"
  }'
```

A \$25,000 transfer skips rule 1 and matches rule 2:

```json theme={null}
{
  "result": "awaiting_approval",
  "allowed": false,
  "reason": "Approval required by policy Transfer limits / rule Approve over $10,000",
  "requires_approval": true,
  "required_approvals": 1,
  "expiry_hours": 24,
  "policy_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "rule_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "trace": [
    {
      "rule_index": 0,
      "rule_name": "Block over $50,000",
      "matched": false,
      "reason": "amount_usd: 25000 not > 50000",
      "failed_condition": { "field": "amount_usd", "operator": "greater_than", "expected": "50000", "actual": "25000" }
    },
    {
      "rule_index": 1,
      "rule_name": "Approve over $10,000",
      "matched": true,
      "terminal": true,
      "action": "require_approval"
    }
  ]
}
```

Run it again with `"amount_usd": "60000"` and you get `"result": "rejected"`, from rule 0. With `"amount_usd": "500"` neither rule matches, so the transfer is allowed: `result` is `approved`, and every rule appears in `trace` with `matched: false`. If any result surprises you, fix the draft with the rule endpoints and dry-run again. [Test rules with dry-run evaluation](https://www.bitnob.dev/docs/platform/policies/test-rules-with-dry-run-evaluation.md) explains every field of the trace.

**5. Validate the stored draft**

[Validate stored draft policy](https://www.bitnob.dev/api-reference/platform/policies/validate-stored-draft-policy.md) runs the same checks as step 3 against what you actually saved.

```bash theme={null}
curl -X POST https://api.platform.bitnob.com/v1/policies/$POLICY_ID/validate \
  -H "X-API-Key: $PLATFORM_API_KEY"
```

Continue only when `valid` is `true` and `issue_count.error` is `0`. Read any warnings. They usually point at a rule order you didn't intend.

**6. Send it for activation**

[Activate policy](https://www.bitnob.dev/api-reference/platform/policies/activate-policy.md) stages the draft for your quorum. The `comment` is recorded on the approval request for your approvers and the audit log.

```bash theme={null}
curl -X POST https://api.platform.bitnob.com/v1/policies/$POLICY_ID/activate \
  -H "X-API-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: activate-transfer-limits-v1" \
  -d '{ "comment": "Initial transfer limits, reviewed by security" }'
```

```json theme={null}
{
  "approval_request_id": "9b2e7a1c-3d4f-4a8b-9c0d-1e2f3a4b5c6d",
  "policy_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "required_approvals": 2,
  "status": "pending_authorization",
  "expires_at": "2026-05-17T10:30:00Z"
}
```

Your quorum approvers approve the request in the dashboard. Wait for the `policy.activated` event, or poll [Get policy](https://www.bitnob.dev/api-reference/platform/policies/get-policy.md) until `status` is `enforced`.

**7. Confirm it is enforced**

Dry-run the \$25,000 case again, this time without `policy_id`, so the engine uses the enforced policy. You should get the same decision as in step 5. Your next real transfer over \$10,000 returns `202` with `"status": "pending_authorization"`.

## What to do next

* Make sure your integration handles the `202` responses this policy now produces: [Handle held and rejected transactions](https://www.bitnob.dev/docs/platform/policies/handle-held-and-rejected-transactions.md).
* Add a time lock for very large transfers: [Delay large transfers with a time lock](https://www.bitnob.dev/docs/platform/policies/delay-large-transfers-with-a-time-lock.md).
* When you need to change the rules: [Change an enforced policy](https://www.bitnob.dev/docs/platform/policies/change-an-enforced-policy.md).


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