Replace the age policy
PUT /v1/age-policy · permission identity-settings:write
Writes the whole age policy. If-Match is required (428 when missing, 412 when it does not match); a policy that was never written has version "0". Rules: min_age and country thresholds are 13-21; country codes are ISO 3166-1 alpha-2 (uppercase), at most 250 entries; parental_consent_below is null or 1..min_age; store_birthdate can be true only when mode is birthdate. The policy applies to later sign-ups only; existing users do not change.
Request
JSON body (unknown fields are rejected with 400 unknown_field):
| Field | Type | Required | Description |
|---|---|---|---|
If-Match |
header | yes | Header: the ETag value from GET /v1/age-policy. |
mode |
string | yes | off, attestation (neutral birth year and month question, value not stored) or birthdate. |
min_age |
integer | yes | 13-21. |
country_min_age |
object | yes | Country code → threshold (13-21); missing countries use min_age. |
country_source |
string | yes | ip or declared. |
under_age_action |
string | yes | block or create_minor. |
parental_consent_below |
integer | null | no | null (no parental consent) or 1..min_age. |
pending_consent_action |
string | yes | block_sign_in or issue_limited. |
store_birthdate |
boolean | yes | true only with mode: birthdate. |
applies_to |
string | yes | self_signup or all. |
Response
200 OK with the policy and the ETag header.
{
"mode": "attestation",
"min_age": 16,
"country_min_age": {"DE": 16, "FR": 15, "IT": 14},
"country_source": "ip",
"under_age_action": "block",
"parental_consent_below": null,
"pending_consent_action": "block_sign_in",
"store_birthdate": false,
"applies_to": "self_signup",
"updated_at": "2026-10-05T09:30:00.123456Z"
}
Tokens
While mode is not off, ID tokens, access tokens and the UserInfo response carry the user's age_group (adult, minor or unknown) whatever the requested scopes, and parental_consent_status (not_required, pending, granted, denied or revoked) when it has a value. With mode: off neither claim is issued. Both are written only by KeenAuth (your hooks and claim templates cannot set them); change them with PATCH /v1/users/{user_id}.
Under pending_consent_action: block_sign_in, a user whose parental_consent_status is pending, denied or revoked gets no token: the authorization request ends with access_denied and the token endpoint answers invalid_grant (Parental consent has not been granted.) for the authorization code, device code and refresh token grants. A refresh token is not revoked; it works again once the consent is granted. Under issue_limited tokens are issued and your application restricts the user from the claims.
Errors
| HTTP | code | When |
|---|---|---|
| 400 | unknown_field |
The body contains an unknown field. |
| 412 | precondition_failed |
If-Match does not match the current version. |
| 415 | unsupported_media_type |
The content type is not application/json. |
| 422 | validation_failed |
A field is missing (required) or breaks a rule above; the pointer names the field, for example /country_min_age/DE. |
| 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
curl -X PUT https://api.eu.keenauth.com/v1/age-policy \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "0"' \
-d '{"mode":"attestation","min_age":16,"country_min_age":{"DE":16,"FR":15},"country_source":"ip","under_age_action":"block","parental_consent_below":null,"pending_consent_action":"block_sign_in","store_birthdate":false,"applies_to":"self_signup"}'