
# Rotate signing keys

`POST /v1/signing-keys:rotate` · permission `signing-keys:write`

Runs a signing key rotation now, outside the automatic schedule.

- `mode: immediate`: the regular rotation step runs at once. The pre-published `next` key starts signing, the old `active` key stays `retired` for 7 days and a new `next` key is published. If `next` uses a different algorithm than the active key (an algorithm change) and was published less than 7 days ago, the call fails with `409 next_key_too_young`.
- `mode: emergency` (suspected key compromise): the old `active` key is revoked and leaves the JWKS **immediately**; `next` becomes active and a new `next` is generated. `revoke_next: true` revokes `next` as well and activates a freshly generated, never-published key. `revoke_sessions: true` also revokes every session and refresh token family of the environment, so users sign in again. `reason` is required.

## Federation keys

`use` can also name one of the environment's federation keys: `saml_sp_signing` and `saml_sp_encryption` (the SAML service provider signing and encryption keys published in your SP metadata) or `oidc_rp_signing` (the `private_key_jwt` key of enterprise OIDC connections, published at `/sso/jwks.json`). These keys are always RSA-3072 and rotate once a year on their own: a new `next` key is published 30 days before it takes over, and the old key stays published for 30 days after.

- `mode: immediate` makes that pre-published `next` key active now; the old key stays published for 30 more days (it appears in `retired_kids`). Without a published `next` key the call fails with `409 conflict`; if the `next` key was published less than 7 days ago, with `409 next_key_too_young`.
- `mode: emergency` removes the active key from the metadata or JWKS **immediately** and destroys its private key (`revoked_kids`). The published `next` key becomes active; if there is none, or with `revoke_next: true`, a new, never-published key does. Identity providers that load your metadata or JWKS from its URL pick the change up on their next refresh. Providers where you uploaded the metadata or certificate by hand reject sign-ins until you upload the new certificate; for that case a `connection.sp_certificate_rotation_started` webhook event is sent for each affected connection.

For a federation key, `next_kid` in the response is `null`, `removed_kids` is empty and `revoke_sessions` is not allowed. If the environment never used SAML or `private_key_jwt`, there is no key to rotate and the call fails with `409 conflict`.

### Connection keys

A SAML connection can have its own dedicated SP keys instead of the environment's. To rotate those, add `connection_id` (`conn_…`) with `use: saml_sp_signing` or `saml_sp_encryption`. The same modes, permissions and rules apply, but only that connection's key set changes: the environment's keys and the other connections' keys stay as they are, and the `connection.sp_certificate_rotation_started` event is sent for that connection only. The audit record names the environment as its target and the connection in `metadata.connection_id`. An unknown connection, or a connection of another environment, fails with `404 not_found`; a connection that uses the environment's keys fails with `409 connection_keys_not_dedicated` (rotate the environment's keys without `connection_id`). Without `connection_id` the environment's key set is rotated.

Emergency mode needs, besides `signing-keys:write`, the `signing-keys:emergency` permission granted explicitly on the credential: only a restricted key or an M2M client carrying that permission can call it. A full-access secret key (`idp_sk_…`) gets `403 insufficient_permissions` in emergency mode.

New token signing keys use the algorithm of the environment policy ([get the policy](/docs/api/management/signing-keys/get-policy)). A successful token signing key rotation emits the `signing_key.rotated` or `signing_key.revoked` webhook event; every rotation writes a management audit record. This endpoint belongs to the "key and secret issuance" rate limit family. Send an `Idempotency-Key`: a retry after a network error then returns the first result instead of rotating twice.

## Request

JSON body (unknown fields are rejected with `400 unknown_field`):

| Field | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | header | no | Recommended. 16-64 characters of `A-Z a-z 0-9 _ -`. |
| `mode` | string | yes | `immediate` or `emergency`. |
| `use` | string | no | The key to rotate. Default `token_signing` (access and ID token signing key). The federation keys are `saml_sp_signing`, `saml_sp_encryption` and `oidc_rp_signing` (see [Federation keys](#federation-keys)). |
| `revoke_sessions` | boolean | no | Emergency and `token_signing` only. Also revoke all sessions and refresh tokens. Default `false`. |
| `revoke_next` | boolean | no | Emergency only. Also revoke the published `next` key. Default `false`. |
| `reason` | string | emergency: yes | Emergency only. 1-512 printable ASCII characters, for example an incident number. Do not put secrets or personal data here. |
| `connection_id` | string | no | `saml_sp_signing` and `saml_sp_encryption` only. A SAML connection (`conn_…`) with dedicated SP keys; its key set is rotated instead of the environment's (see [Connection keys](#connection-keys)). |

With `mode: immediate`, sending `revoke_sessions: true`, `revoke_next: true` or a `reason` fails with `422` (`not_allowed`). A `connection_id` with `token_signing` or `oidc_rp_signing` fails the same way.

## Response

`200 OK` with the result of the rotation. `activated_kid` is the key that signs from now on and `next_kid` the newly published `next` key (`null` for a federation key). `retired_kids` are keys that stopped signing and stay published (7 days for a token signing key, 30 days for a federation key); `revoked_kids` are keys removed from the JWKS by an emergency rotation; `removed_kids` are retired keys whose 7-day window ended and that left the JWKS in the same step.

```json
{
  "mode": "immediate",
  "use": "token_signing",
  "activated_kid": "ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs",
  "next_kid": "GKw-c0PwFokMUQ6T-TUmEWnZ4_VlQ2Qpgw-vCTT0-OQ",
  "retired_kids": ["PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0"],
  "revoked_kids": [],
  "removed_kids": ["Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y"],
  "revoke_sessions": false
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 400 | `unknown_field` | The body contains an unknown field. |
| 400 | `malformed_request` | The body is not valid JSON, or the `Idempotency-Key` header is malformed or repeated. |
| 403 | `insufficient_permissions` | The credential lacks `signing-keys:write`, or emergency mode was requested without an explicitly granted `signing-keys:emergency`. |
| 404 | `not_found` | `connection_id` names no SAML connection of this environment. |
| 409 | `connection_keys_not_dedicated` | `connection_id` names a connection that uses the environment's SP keys. |
| 409 | `next_key_too_young` | An algorithm change is pending and the `next` key was published less than 7 days ago, or (federation key, `immediate`) the `next` key was published less than 7 days ago. |
| 409 | `conflict` | The environment has no published `next` key, has no active key of the federation `use`, or the previous federation key is still in its 30-day overlap. |
| 415 | `unsupported_media_type` | The content type is not `application/json`. |
| 422 | `validation_failed` | A field is missing or invalid; the pointer names the field, for example `/reason`. |
| 422 | `idempotency_key_reuse` | The `Idempotency-Key` was already used with a different body. |
| 501 | `not_implemented` | The operation is not available in this deployment yet. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 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

Regular rotation:

```bash
curl -X POST "https://api.eu.keenauth.com/v1/signing-keys:rotate" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode":"immediate"}'
```

Emergency rotation with a restricted key that grants `signing-keys:write` and `signing-keys:emergency`:

```bash
curl -X POST "https://api.eu.keenauth.com/v1/signing-keys:rotate" \
  -H "Authorization: Bearer $KEENAUTH_EMERGENCY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode":"emergency","revoke_sessions":true,"reason":"INC-2041 key exposure"}'
```

Emergency rotation of the SAML SP signing key, also revoking the published `next` key:

```bash
curl -X POST "https://api.eu.keenauth.com/v1/signing-keys:rotate" \
  -H "Authorization: Bearer $KEENAUTH_EMERGENCY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode":"emergency","use":"saml_sp_signing","revoke_next":true,"reason":"INC-2042 SP key exposure"}'
```

Emergency rotation of the dedicated SAML SP signing key of one connection:

```bash
curl -X POST "https://api.eu.keenauth.com/v1/signing-keys:rotate" \
  -H "Authorization: Bearer $KEENAUTH_EMERGENCY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"mode":"emergency","use":"saml_sp_signing","connection_id":"conn_01h2xcejqtf2nbrexx3vqjhp41","reason":"INC-2043 connection key exposure"}'
```
