
# 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.

```json
{
  "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

```bash
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"}'
```
