
# Preview an organization policy

`POST /v1/organizations/{org_id}/policies/{policy_kind}:preview` · permission `organizations:write`

Evaluates a candidate settings object without saving it. The body is the same as for [creating or replacing a policy](/docs/api/management/organization-policies/put), and the server checks it with the same rules and the same plan gate, so a candidate that `PUT` would reject is rejected here with the same problem code. Nothing is written: no policy, no audit entry and no event. The request takes no `If-Match` and no `Idempotency-Key`, and the response carries no `ETag`.

The response counts the users the candidate applies to (`basis` `users_in_scope`), not the users who would be blocked, with one exception: `phishing_resistant` (see below). Conditions that are only known at sign-in, or that the policy service does not hold, are not filtered out; `not_evaluated` lists them so that the count is never read as more than it is.

| Kind | Users counted | `not_evaluated` |
|---|---|---|
| `authentication_methods` | Active members of the organization | `credentials` |
| `mfa` (`required_for` `admins` or `all`) | Active members of the organization | `mfa_enrollment`; also `admin_scope` for `admins` |
| `phishing_resistant` (`required_for` `admins` or `all`) | Active members of the organization without an accepted WebAuthn credential (`basis` `users_without_webauthn`, see below) | `credential_policy`; also `admin_scope` for `admins` |
| `sso_enforcement` `org_context` | Active members of the organization, minus `bypass_user_ids` | `federated_sign_in` |
| `sso_enforcement` `verified_domains` | Active members, plus active users of the environment with an email address on a verified domain of this organization, minus `bypass_user_ids` | `federated_sign_in` |
| `session` | Active members of the organization | none |
| `ip_allowlist` | Active members of the organization | `client_ip`; also `admin_scope` for `admins` |

For `phishing_resistant`, the count and the sample are the users in scope who cannot sign in phishing-resistantly today: users without an active WebAuthn credential (a passkey, or a security key used with a first factor) that the AAGUID allowlist accepts. The allowlist is the candidate's, or the environment's when the candidate's is empty; with an allowlist, a credential counts only if its model is listed and its attestation was verified. Pending, revoked and clone-suspected credentials do not count. `credential_policy` means that the environment's attestation requirement, the device-bound passkey rule and the authenticator model status are not applied, so the count can be lower than the number of users who would be blocked. When the scope has more than 10,000 users, the response falls back to `users_in_scope` with `passkey_enrollment`. If the credential data cannot be read, the request fails with `503` and no count is returned.

`required_for` `none` and `mode` `off` count nobody. An active member is a member whose membership is active and whose user is not blocked; each user is counted once. `admin_scope` means that `admins` is counted as all members, which is how sign-in applies it today. The users of another organization or another environment are never counted.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `org_id` | path | yes | Organization ID (`org_…`). |
| `policy_kind` | path | yes | Policy kind: `authentication_methods`, `mfa`, `phishing_resistant`, `sso_enforcement`, `session` or `ip_allowlist`. Any other value returns `404`. |
| `settings` | object | yes | The candidate settings object of the kind, with the same fields as for `PUT`. |

Request body:

```json
{
  "settings": {
    "required_for": "all",
    "allowed_factors": [
      "totp",
      "webauthn"
    ],
    "enrollment_grace_days": 7
  }
}
```

## Response

`200 OK`. `sample` holds at most 20 users of the counted set, ordered by ID, with the user ID only.

| Field | Type | Description |
|---|---|---|
| `organization_id` | string | Organization ID (`org_…`). |
| `policy_kind` | string | The previewed kind. |
| `basis` | string | What `affected_count` counts: `users_in_scope` or, for `phishing_resistant`, `users_without_webauthn`. New values may be added. |
| `affected_count` | integer | Number of users that `basis` counts. |
| `sample` | array | At most 20 objects with `user_id` (`usr_…`). |
| `not_evaluated` | array | Conditions the count does not filter: `admin_scope`, `credentials`, `mfa_enrollment`, `passkey_enrollment`, `federated_sign_in`, `client_ip`, `credential_policy`. New values may be added. |

```json
{
  "organization_id": "org_01j9xk3m7q8r2s4t6v8w0y2z4a",
  "policy_kind": "mfa",
  "basis": "users_in_scope",
  "affected_count": 2,
  "sample": [
    {
      "user_id": "usr_01j9xk3m7q8r2s4t6v8w0y2z4b"
    },
    {
      "user_id": "usr_01j9xk3m7q8r2s4t6v8w0y2z4c"
    }
  ],
  "not_evaluated": [
    "mfa_enrollment"
  ]
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body has a field that is not in the schema. |
| 403 | `plan_feature_unavailable` | The kind or the AAGUID allowlist requires a higher plan; the problem carries `feature` and `required_plan`. |
| 404 | `not_found` | The organization does not exist or the kind is unknown. |
| 409 | `domain_not_verified` | `sso_enforcement` with `mode` `verified_domains` needs a verified domain on the organization. |
| 409 | `connection_test_required` | Turning `sso_enforcement` on (from `off` or from no policy) needs a successful test sign-in of the current configuration of at least one of the `connection_ids` in the last 24 hours. Run a connection test (`POST /v1/connections/{connection_id}:test`) and try again. |
| 409 | `organization_pending_deletion` | The organization is waiting for self-service deletion. |
| 422 | `validation_failed` | A settings field is unknown or invalid; `errors[]` points at `/settings/…`. |
| 422 | `policy_weaker_than_parent` | The setting is weaker than the environment's setting; `errors[]` points at the weaker field. |
| 422 | `at_least_one_method` | `authentication_methods` leaves no sign-in method. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable (for `phishing_resistant`, also when the credential data cannot be read); retry after the `Retry-After` period. |

## Example

```bash
curl -X POST https://api.eu.keenauth.com/v1/organizations/org_01j9xk3m7q8r2s4t6v8w0y2z4a/policies/mfa:preview \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"settings":{"required_for":"all","allowed_factors":["totp","webauthn"]}}'
```

```bash
keenauth organizations policies preview org_01j9xk3m7q8r2s4t6v8w0y2z4a mfa \
  --data '{"settings":{"required_for":"all","allowed_factors":["totp","webauthn"]}}'
```
