Update the environment settings
PATCH /v1/environment-settings · permission environment-settings:write
Updates the environment settings with a JSON merge patch (application/merge-patch+json, RFC 7396). If-Match is required: missing gives 428, a stale version gives 412. If the settings were never written the version is "0" and the first write creates the row. A patch that changes nothing writes nothing and keeps the version.
Request
| Field | Type | Required | Description |
|---|---|---|---|
If-Match |
header | yes | The ETag of the settings as last read ("0" if never written). Missing: 428; stale: 412. |
locales.enabled |
string[] | no | Enabled language codes (registered languages), 1-64 entries, no duplicates. Cannot be null. |
locales.default |
string | no | Default language; must be one of locales.enabled. Cannot be null. |
default_zoneinfo |
string | no | IANA time zone name (default UTC). Cannot be null. |
session.idle_timeout_s |
integer | no | Idle timeout in seconds, 60-8640000 (default 259200); not greater than session.absolute_timeout_s. Cannot be null. |
session.absolute_timeout_s |
integer | no | Absolute lifetime in seconds, 60-31536000 (default 2592000). Cannot be null. |
session.max_concurrent |
integer | no | Concurrent sessions per user, 0-500; 0 means the system limit (default). Cannot be null. |
session.concurrent_mode |
string | no | evict_oldest (default) or deny_new. Cannot be null. |
session.anomaly_action |
string | no | enforce (default) or log. Cannot be null. |
signup.mode |
string | no | open (default), invite_only or closed. Cannot be null. |
signup.email_domain_mode |
string | no | off (default), allowlist or denylist. Cannot be null. |
signup.email_domains |
string[] | null | no | At most 1000 domains; replaces the whole list (RFC 7396 does not merge arrays). null or [] clears it. Entries are normalized (lowercase, IDNA A-label), duplicates are dropped and the list is stored sorted. Kept while the mode is off. |
security_notifications.enabled |
boolean | no | Master switch (default true). Cannot be null. |
security_notifications.events |
string[] | null | no | Subset of the 12 security events (password_changed, password_reset, email_changed, phone_changed, authenticator_added, authenticator_removed, recovery_code_used, recovery_codes_regenerated, account_locked, breached_password_detected, new_device_sign_in, account_recovery_started); replaces the whole list and is returned in canonical order. null or [] clears it (no event). |
account_recovery.mode |
string | no | self_service_waiting (default), admin_only or disabled. Cannot be null. |
account_recovery.waiting_hours |
integer | no | 24-168 hours (default 72); stored in every mode. Cannot be null. |
account_recovery.support_url |
string | no | Empty or an https:// URL of at most 2048 bytes, without user info or fragment. Shown when recovery is disabled. Cannot be null. |
mfa.email_second_factor_enabled |
boolean | no | Email second factor on or off (default false). Cannot be null. |
mfa.email_second_factor_acr |
string | no | Assurance the email factor counts as: 1fa (default) or mfa. Cannot be null. |
mfa.totp_issuer_label |
string | no | TOTP issuer label, at most 64 characters; empty means the environment's display name. No control characters, : or leading/trailing spaces. Cannot be null. |
mfa.recovery_codes_enabled |
boolean | no | Recovery codes on or off (default true). Cannot be null. |
mfa.recovery_code_count |
integer | no | Number of recovery codes, 8-16 (default 10). Cannot be null. |
mfa.required |
boolean | no | Require a second factor at every sign-in in the environment (default false). Combined with OR with the application, organization and request requirements; an existing SSO session below it is sent to step up (interaction_required with prompt=none). Cannot be null. |
mfa.min_acr |
string | no | Lowest assurance every sign-in in the environment must reach: 1fa (default, no effect), mfa or phr. The highest of the environment, application, organization and request thresholds applies, to new sign-ins and to SSO session reuse. Cannot be null. |
mfa.enrollment_grace_days |
integer | no | Days a user without a second factor may keep signing in without enrolling, counted from the first time enrollment is required of them, 0-14 (default 0, no grace). The grace postpones only what the environment and organization axes require; an application's min_acr and the requested acr_values are never postponed. Out of range returns 422 validation_failed with out_of_range. Shortening it takes effect at once. Cannot be null. |
frontchannel_logout_enabled |
boolean | no | Front-Channel Logout on or off (default false); turning it off keeps the registered URIs. Cannot be null. |
allowed_first_factors |
string[] | no | Allowed first factors: at least one of password, passkey, email_otp, magic_link, sms_otp, social, enterprise, no duplicates. Replaces the whole list and is returned in this order. Default password, passkey, social, enterprise; email_otp, magic_link and sms_otp are on only when added explicitly. An empty list returns 422 required (an environment nobody could sign in to). Cannot be null. |
Behavior
- Groups merge recursively: send only the fields you change inside
locales,session,signup,security_notifications,account_recoveryandmfa. A group itself cannot benull. - Shortening a session lifetime, lowering the concurrent session limit, narrowing sign-up or account recovery, lowering the email second factor from
mfato1fa, and removing a factor fromallowed_first_factorsapply to new sessions and sign-ins immediately; existing sessions keep their own lifetime. - Validation errors carry the JSON pointer of the field, for example
/session/idle_timeout_swith codeout_of_range. An error in a list entry points at the entry:/allowed_first_factors/1withunknown_value(unknown value) orduplicate(repeated value). allowed_first_factorsis intersected with application and organization policies: they can only narrow this set.- If the new set leaves no method in common with an application that has its own
allowed_first_factors, the request returns422 policy_conflict; change the application's set first. - The audit record and the change event carry the names of the changed fields, never their values. Attack protection is a separate resource and is not part of this one.
Response
200 OK with the updated settings and the new ETag.
{
"locales": {"enabled": ["en", "tr"], "default": "tr"},
"default_zoneinfo": "Europe/Istanbul",
"session": {"idle_timeout_s": 3600, "absolute_timeout_s": 2592000, "max_concurrent": 5, "concurrent_mode": "evict_oldest", "anomaly_action": "enforce"},
"signup": {"mode": "invite_only", "email_domain_mode": "allowlist", "email_domains": ["example.com"]},
"security_notifications": {"enabled": true, "events": ["password_changed", "password_reset"]},
"account_recovery": {"mode": "self_service_waiting", "waiting_hours": 72, "support_url": ""},
"mfa": {"email_second_factor_enabled": false, "email_second_factor_acr": "1fa", "totp_issuer_label": "Acme", "recovery_codes_enabled": true, "recovery_code_count": 10, "required": false, "min_acr": "1fa", "enrollment_grace_days": 0},
"frontchannel_logout_enabled": false,
"allowed_first_factors": ["password", "passkey", "magic_link", "enterprise"],
"updated_at": "2026-10-05T09:30:00.123456Z"
}
Errors
| HTTP | code | When |
|---|---|---|
| 400 | unknown_field |
The patch contains an unknown or read-only field, or is not a JSON object (malformed_request). |
| 412 | precondition_failed |
If-Match does not match the current version. |
| 415 | unsupported_media_type |
The content type is not application/merge-patch+json. |
| 422 | validation_failed |
A value is invalid (the pointer names the field), or a field that cannot be null was set to null (code: invalid). |
| 422 | policy_conflict |
The new allowed_first_factors leaves no method in common with an application that has its own set. |
| 428 | precondition_required |
The If-Match header is missing. |
| 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
Turn on email magic links and turn off passwords:
curl -X PATCH https://api.eu.keenauth.com/v1/environment-settings \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/merge-patch+json" \
-H 'If-Match: "0"' \
-d '{"allowed_first_factors":["passkey","magic_link","social","enterprise"]}'
Session and sign-up settings:
curl -X PATCH https://api.eu.keenauth.com/v1/environment-settings \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/merge-patch+json" \
-H 'If-Match: "0"' \
-d '{"session":{"idle_timeout_s":3600},"signup":{"mode":"invite_only","email_domain_mode":"allowlist","email_domains":["example.com"]}}'