
# Rotate an API key

`POST /v1/api-keys/{key_id}:rotate` · permission `api-keys:write` · optional `Idempotency-Key`

Rotates a key: creates a NEW key with the same name, type, permissions and `allowed_cidrs`, and returns it. The old key stays valid for `overlap_hours`, then stops working.

- **New key.** The response describes the new key; `rotated_from` is the old key's id and `key` carries the raw key once (`Cache-Control: no-store`). Deploy it, then let the old key lapse.
- **Overlap.** The old key gets `revoked_at` in the future and `revoke_reason: rotated`; with `overlap_hours: 0` it is revoked immediately.
- **No escalation.** A key cannot rotate a key whose permissions exceed its own, and a `restricted` key cannot rotate a `secret` key. The new key cannot expire after the calling key, even when a key rotates itself (extend a lifetime in the dashboard), and a calling key with `allowed_cidrs` can only rotate keys limited to networks inside its own. A key created in the dashboard, and every key rotated from it, can be rotated only by itself or in the dashboard (`permission_escalation`). The new key keeps the old key's `created_by`; the rotating key is recorded in the audit log.
- **Idempotent replay.** A replay with the same `Idempotency-Key` omits `key` and returns `secret_redacted: true`.

## Request

| Parameter | In | Description |
|---|---|---|
| `key_id` | path | API key id (`key_…`). |

Body:

The body is optional; without it all defaults apply.

| Field | Type | Description |
|---|---|---|
| `overlap_hours` | integer | Optional, 0–168, default 24. How many hours the old key stays valid. |
| `expires_at` | string (date-time) | Optional. Expiry of the NEW key; default now + 365 days, at most the calling key's expiry and at most 730 days. |

## Response

`200 OK` with `Cache-Control: no-store`. The NEW key object plus `key`.

```json
{
  "id": "key_0000000000000000000000001z",
  "name": "ci",
  "type": "restricted",
  "permissions": ["users:read"],
  "allowed_cidrs": [],
  "last4": "xxxx",
  "created_by": "key_…",
  "created_at": "2026-10-01T10:00:00Z",
  "expires_at": "2027-10-01T10:00:00Z",
  "last_used_at": null,
  "last_used_ip": null,
  "rotated_from": "key_…",
  "revoked_at": null,
  "revoke_reason": null,
  "key": "<shown once>"
}
```

## Errors

| HTTP | `code` | When |
|---|---|---|
| 401 | `unauthenticated` | The key is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The key lacks the required permission. |
| 403 | `permission_escalation` | The request would give a key more permissions, a wider network range or a later expiry than the calling key has, a `restricted` key tries to create, rotate or revoke beyond its own permissions, or another key tries to rotate or revoke a key created in the dashboard. |
| 404 | `not_found` | The key does not exist in this environment. |
| 409 | `conflict` | The key is already revoked, expired or already rotated (not active). |
| 409 | `api_key_limit_reached` | The environment already has the maximum of 50 active keys. |
| 422 | `validation_failed` | The body is invalid: `overlap_hours` outside 0–168 or `expires_at` in the past or more than 730 days ahead. |

## Example

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