# Policies API reference

> The policy operations of the Fax for Agents API. Create a delegated policy, version it, dry-run a send against it, and set auto-send for connected apps.

Page: https://faxforagents.com/docs/api/reference/policies/
Last changed: 2026-11-13

Updated Nov 13, 2026

9 operations of the Fax for Agents API, from the OpenAPI document version 1.20.0, snapshot of 2026-11-13. The base URL is `https://api.faxforagents.com/v1`. This is one group of the [full reference](https://faxforagents.com/docs/api/reference/).

- Changing a policy creates a new immutable version, and earlier versions stay readable.
- A policy evaluation is a dry run that never executes a send.
- The purpose of a policy is shown to people and is never evaluated.

## Operations

- `GET /v1/organizations/current/auto-send` Read the auto-send setting for connected apps in this environment
- `PUT /v1/organizations/current/auto-send` Let connected apps send without asking, inside two limits (owner)
- `GET /v1/policies` List the organization's policies
- `POST /v1/policies` Create a delegated policy (version 1)
- `GET /v1/policies/effective` Read the rules bound to the caller, an API key, an agent or a connected app
- `GET /v1/policies/{policy_id}` Read a policy (its current version)
- `PATCH /v1/policies/{policy_id}` Change a policy: a new immutable version
- `GET /v1/policies/{policy_id}/versions` Every version, oldest first
- `POST /v1/policy-evaluations` Dry run: would the calling key's policies allow this send? Never executes

## What does a policy decide?

Whether a send may go out without a person. A live key that has no policy needs an approval for each fax. A delegated policy lists limits, and a send inside every one of them is allowed at once. The server evaluates the limits from the quote and the document. Nothing the agent writes is an input.

`POST /v1/policies` creates a policy at version 1 with a `name`, a `purpose` and `rules`. The purpose is for the people who read the list later. The rules are what counts. The [policies guide](https://faxforagents.com/docs/policies/) has each rule field and the order they are checked in.

## Why are policy versions immutable?

So that a job can say which rules allowed it. `PATCH /v1/policies/{policy_id}` does not edit the rules in place. It writes a new version, and the policy points at it in `current_version_id`. `GET /v1/policies/{policy_id}/versions` returns every version, oldest first.

A send that a policy allowed records the version. Months later the record still shows the limits as they were on that day, even if the policy has changed ten times since.

`enabled` turns a policy off without deleting it. A disabled policy allows nothing.

## How do I test a send against the rules?

`POST /v1/policy-evaluations` is a dry run. It takes a `quote_id`, or a plan made of a `document_id`, a `to` and a `from`. The answer has the `decision`, the policy and version that decided, the `rule` that failed first and every denial in `denials`. `executed` is false. The call never sends.

The verdict is for the calling key. An agent can run it before it asks a person, and skip the request when the answer is allow.

## What may this key do on its own?

`GET /v1/policies/effective` answers that. Without parameters it returns the rules bound to the caller. With `key_id`, `agent_id` or `connection_id` an administrator reads them for another key, an agent or a connected app. `approval_required_without_policy` says whether a person is needed for whatever the policies do not cover, and `rules_order` names the order of evaluation.

## What is the auto-send setting?

A managed policy for connected apps. `PUT /v1/organizations/current/auto-send` lets the connected apps of the organization send without asking, inside two limits. One is an amount per fax in `max_amount_micro`. The other is a number of faxes per UTC day in `daily_max_jobs`. Connected apps are OAuth connections, such as ChatGPT or Claude, and API keys flagged `connected_app`.

The description of the operation sets out the rest. The environment comes from `X-FFA-Environment`. The first enable creates the managed policy. Every later save is a new policy version with its audit row. A hosted integration the server marks `requires_human_approval` still needs a person. A browser session needs a sign-in from the last 15 minutes.

`GET /v1/organizations/current/auto-send` reads the setting with the policy id and the version behind it.

## Which scopes guard policies?

Reads and the dry run need `policy:read`, which the default API key carries. Creating and changing a policy need `policy:admin`. The auto-send write needs `org:admin`, the owner's scope. An agent key is never issued `policy:admin` or `org:admin`, so an agent can read its limits and test against them, and cannot move them.

## Each operation in detail

### GET `/v1/organizations/current/auto-send`

Read the auto-send setting for connected apps in this environment

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Responses

GET /v1/organizations/current/auto-send responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/organizations/current/auto-send response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `daily_max_jobs` | integer | yes | Faxes per UTC day, at most, across every connected app. |
| `enabled` | boolean | yes |  |
| `environment` | string (live, test) | yes |  |
| `max_amount_micro` | integer | yes | Amount per fax, at most, in micro-units of USD. |
| `policy_id` | string or null | yes | The managed policy; null before the first enable. |
| `version` | integer or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/organizations/current/auto-send \ -H "Authorization: Bearer ffa_test_..."`
```

### PUT `/v1/organizations/current/auto-send`

Let connected apps send without asking, inside two limits (owner)

The environment comes from X-FFA-Environment. When enabled, every connected app of the organization (OAuth connections such as ChatGPT or Claude, and API keys flagged connected_app) sends in that environment with no approval per fax, inside the amount per fax and the faxes per day. A hosted integration the server marks requires_human_approval still needs a person. The first enable creates a managed policy; every later save is a new policy version with its audit row. A browser session needs a sign-in from the last 15 minutes.

Credential: an API key or a signed-in session. Scope: `org:admin`.

#### Parameters

PUT /v1/organizations/current/auto-send parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PUT /v1/organizations/current/auto-send request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `daily_max_jobs` | integer | no | Faxes per UTC day. |
| `enabled` | boolean | yes |  |
| `max_amount_micro` | integer | no | Amount per fax, at most, in micro-units of USD (5000000 = $5.00). |

#### Responses

PUT /v1/organizations/current/auto-send responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PUT /v1/organizations/current/auto-send response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `daily_max_jobs` | integer | yes | Faxes per UTC day, at most, across every connected app. |
| `enabled` | boolean | yes |  |
| `environment` | string (live, test) | yes |  |
| `max_amount_micro` | integer | yes | Amount per fax, at most, in micro-units of USD. |
| `policy_id` | string or null | yes | The managed policy; null before the first enable. |
| `version` | integer or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/organizations/current/auto-send \ -X PUT \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"enabled":false}'`
```

### GET `/v1/policies`

List the organization's policies

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Responses

GET /v1/policies responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/policies response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object Policy | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/policies`

Create a delegated policy (version 1)

Credential: an API key or a signed-in session. Scope: `policy:admin`.

#### Parameters

POST /v1/policies parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/policies request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | no |  |
| `name` | string | yes |  |
| `purpose` | string or null | no | Why this policy exists. Shown, never evaluated. |
| `rules` | object | yes | See the PolicyRules schema in docs/APPROVALS.md. |

#### Responses

POST /v1/policies responses

| Status | Meaning |
| --- | --- |
| `201` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/policies response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `current_version_id` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `managed` | string or null | no | connected_apps when a Settings switch (the auto-send setting) manages this policy. Such a policy is read here and changed only through PUT /v1/organizations/current/auto-send. |
| `name` | string | yes |  |
| `purpose` | string or null | yes |  |
| `rules` | object | yes |  |
| `updated_at` | string | yes |  |
| `version` | integer | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{"name":"example","rules":{}}'`
```

### GET `/v1/policies/effective`

Read the rules bound to the caller, an API key, an agent or a connected app

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Parameters

GET /v1/policies/effective parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `key_id` | query | string or null | no | An API key in your organization. Uses the key's environment. Choose either key_id or agent_id. |
| `agent_id` | query | string or null | no | An agent in your organization. Uses the current environment and excludes bindings to individual keys. |
| `connection_id` | query | string or null | no | A connected app (an OAuth grant, GET /v1/oauth/grants) in your organization; a signed-in person sees their own. Uses the connection's environment. Choose at most one of key_id, agent_id and connection_id. |

#### Responses

GET /v1/policies/effective responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/policies/effective response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `actor` | string | yes |  |
| `approval_required_without_policy` | boolean | yes |  |
| `environment` | string (live, test) | yes |  |
| `policies` | array of object Policy | yes |  |
| `requires_human_approval` | boolean | no | Server-set for hosted integration clients and their keys: every send needs a person's approval, whatever the policies say. |
| `rules_order` | array of string | yes |  |
| `under_policy` | boolean | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies/effective \ -H "Authorization: Bearer ffa_test_..."`
```

### GET `/v1/policies/{policy_id}`

Read a policy (its current version)

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Parameters

GET /v1/policies/{policy_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `policy_id` | path | string | yes |  |

#### Responses

GET /v1/policies/{policy_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/policies/{policy_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `current_version_id` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `managed` | string or null | no | connected_apps when a Settings switch (the auto-send setting) manages this policy. Such a policy is read here and changed only through PUT /v1/organizations/current/auto-send. |
| `name` | string | yes |  |
| `purpose` | string or null | yes |  |
| `rules` | object | yes |  |
| `updated_at` | string | yes |  |
| `version` | integer | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies/POLICY_ID \ -H "Authorization: Bearer ffa_test_..."`
```

### PATCH `/v1/policies/{policy_id}`

Change a policy: a new immutable version

Credential: an API key or a signed-in session. Scope: `policy:admin`.

#### Parameters

PATCH /v1/policies/{policy_id} parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `policy_id` | path | string | yes |  |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

PATCH /v1/policies/{policy_id} request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean or null | no |  |
| `name` | string or null | no |  |
| `purpose` | string or null | no |  |
| `rules` | object or null | no |  |

#### Responses

PATCH /v1/policies/{policy_id} responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

PATCH /v1/policies/{policy_id} response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `created_at` | string | yes |  |
| `created_by` | string | yes |  |
| `current_version_id` | string or null | yes |  |
| `enabled` | boolean | yes |  |
| `id` | string | yes |  |
| `managed` | string or null | no | connected_apps when a Settings switch (the auto-send setting) manages this policy. Such a policy is read here and changed only through PUT /v1/organizations/current/auto-send. |
| `name` | string | yes |  |
| `purpose` | string or null | yes |  |
| `rules` | object | yes |  |
| `updated_at` | string | yes |  |
| `version` | integer | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies/POLICY_ID \ -X PATCH \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```

### GET `/v1/policies/{policy_id}/versions`

Every version, oldest first

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Parameters

GET /v1/policies/{policy_id}/versions parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `policy_id` | path | string | yes |  |

#### Responses

GET /v1/policies/{policy_id}/versions responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

GET /v1/policies/{policy_id}/versions response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | array of object PolicyVersion | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policies/POLICY_ID/versions \ -H "Authorization: Bearer ffa_test_..."`
```

### POST `/v1/policy-evaluations`

Dry run: would the calling key's policies allow this send? Never executes

Credential: an API key or a signed-in session. Scope: `policy:read`.

#### Parameters

POST /v1/policy-evaluations parameters

| Name | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | no | Replay the first successful response for the same request within 24 hours. Reusing the idempotency header with a different request returns 409 idempotency_key_conflict. Responses include Idempotent-Replayed: true on replay. |

#### Request body application/json, required

POST /v1/policy-evaluations request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `document_id` | string or null | no | Or plan: this document ... |
| `from` | string or null | no |  |
| `quote_id` | string or null | no | Evaluate an existing quote. |
| `to` | string or null | no | ... to this E.164 destination. |

#### Responses

POST /v1/policy-evaluations responses

| Status | Meaning |
| --- | --- |
| `200` | Successful Response |
| `401` | No credential, or one that was not accepted: invalid_token for an API key, unauthenticated where a signed-in session is needed. error.details.reason says why a key was refused. |
| `403` | The credential is valid and is not allowed to do this. scope_missing puts the scope in error.details.scope. Other codes are agent_forbidden and approval_required. A session can also meet csrf_invalid and reauth_required. |
| `404` | not_found: no such resource for this credential. A resource of another organization answers the same. |
| `409` | The resource is in a state that refuses the call, or the Idempotency-Key was used with a different request (idempotency_key_conflict). error.code names the state. |
| `422` | A field failed validation: invalid_request, with one entry per field in error.details, or a code that names the rule. |
| `429` | rate_limited. Retry-After and the RateLimit-* headers say when to try again. |

#### Response fields

POST /v1/policy-evaluations response fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `decision` | string | yes |  |
| `denials` | array of object | yes |  |
| `executed` | boolean | no |  |
| `plan` | object | yes |  |
| `policy_id` | string or null | yes |  |
| `policy_name` | string or null | yes |  |
| `policy_version_id` | string or null | yes |  |
| `reason` | string | yes |  |
| `rule` | string or null | yes |  |
| `version` | integer or null | yes |  |

#### Example

```
`curl https://api.faxforagents.com/v1/policy-evaluations \ -X POST \ -H "Authorization: Bearer ffa_test_..." \ -H "Content-Type: application/json" \ -d '{}'`
```
