
# Create or replace an organization policy

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

Replaces the whole settings object of one policy kind. An organization policy can only be stricter than the environment's setting. The plan gate is enforced by the server: the `session` and `ip_allowlist` kinds and the AAGUID allowlist require Enterprise (open in development environments). The settings content is written to the management audit log; the `organization.updated` event does not carry it.

## 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`. |
| `If-Match` | header | no | Required when the policy exists (`ETag` of the last read); omit it to create the policy. `*` matches an existing policy. |
| `settings` | object | yes | The kind's settings object. Fields by kind: `authentication_methods`: `allowed_connection_ids` (`conn_…`), `allow_passkey`, `allow_password`; `mfa`: `required_for` (`none`, `admins`, `all`), `allowed_factors` (`totp`, `webauthn`, `sms`, `email`), `enrollment_grace_days` (0-30); `phishing_resistant`: `required_for`, `aaguid_allowlist` (at most 500); `sso_enforcement`: `mode` (`off`, `org_context`, `verified_domains`), `connection_ids` (`conn_…`), `bypass_user_ids` (`usr_…`, at most 2); `session`: `idle_minutes`, `absolute_minutes`, `refresh_idle_days`, `refresh_absolute_days`; `ip_allowlist`: `cidrs` (at most 100), `applies_to` (`admins`, `all`). |

Request body:

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

## Response

`201 Created` when the policy was created, `200 OK` when it was replaced; the body is the policy and the response carries its new `ETag`.

```json
{
  "organization_id": "org_01j9xk3m7q8r2s4t6v8w0y2z4a",
  "policy_kind": "mfa",
  "schema_version": 1,
  "settings": {
    "required_for": "all",
    "allowed_factors": [
      "totp",
      "webauthn"
    ],
    "enrollment_grace_days": 7
  },
  "updated_at": "2026-10-12T09:30:00.123456Z",
  "version": 1
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body has a field that is not in the schema or is read-only. |
| 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; cancel the deletion first. |
| 412 | `precondition_failed` | `If-Match` does not match the current version, or was sent while no policy of this kind exists, or another request created the policy while this request was creating it; read the policy again. |
| 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. |
| 428 | `precondition_required` | `If-Match` is missing while the policy exists. |
| 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; retry after the `Retry-After` period. |

## Example

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