
# Update the signing key policy

`PATCH /v1/signing-keys/policy` · permission `signing-keys:write`

Updates the signing key policy 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 policy was never written the version is `"0"`. A patch that changes nothing writes nothing and keeps the version.

Changing `algorithm` generates a new `next` key with the new algorithm right away; it can become active at the earliest 7 days later, so verifiers that cache the JWKS see it first (an earlier `immediate` rotation fails with `409 next_key_too_young`). The active key keeps signing until then. Changing `rotation_interval_days` moves `next_rotation_at`. A successful change writes the `signing_key.policy_updated` management audit record.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `If-Match` | header | yes | The `ETag` of the policy as last read (`"0"` if never written). Missing: `428`; stale: `412`. |
| `algorithm` | string | no | `RS256`, `ES256`, `PS256` or `EdDSA`. Cannot be `null`. |
| `rotation_interval_days` | integer | no | 30-365. Cannot be `null`. |
| `storage` | string | no | `wrapped` (the private key is stored encrypted under the environment key and used only inside the signing service). `transit` (key held in the key management service) is not available yet and is rejected with `422`. Cannot be `null`. |

## Response

`200 OK` with the updated policy and the new `ETag`.

```json
{
  "algorithm": "ES256",
  "rotation_interval_days": 90,
  "storage": "wrapped",
  "next_rotation_at": "2026-12-04T09:30:00.123456Z",
  "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 (`invalid_value`), out of range (`out_of_range`), set to `null` (`invalid`), or `storage` is `transit`. |
| 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. |
| 501 | `not_implemented` | The operation is not available in this deployment yet; retrying does not change the result. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry after the `Retry-After` period. |

## Example

```bash
curl -X PATCH https://api.eu.keenauth.com/v1/signing-keys/policy \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/merge-patch+json" \
  -H 'If-Match: "0"' \
  -d '{"algorithm":"ES256","rotation_interval_days":90}'
```
