# POST /v1/api-keys/:keyId/rotate (/docs/api/reference/api-keys/rotate)



<Endpoint method="POST" path="/v1/api-keys/{keyId}/rotate" auth="X-Api-Key" phase="1" />

**Planned rotation.** Replaces both the prefix and the hashed secret. The old secret is instantly invalid — any consumer using it gets `401 UNAUTHENTICATED` on the next request. Rotation also re-activates the key: `kill_switch` is cleared, `revoked_at` is nulled, `is_active` flips back to `true`.

Use this for calendar-driven rotation (quarterly / annual), post-leak recovery after a `kill`, or bootstrapping a fresh secret for a rotation drill. For active incident response where you want the key dead *immediately* and recoverable only by a human, use [`/kill`](/docs/api/reference/api-keys/kill).

The new plaintext secret is returned **exactly once** in the response body. Store it immediately — Layers cannot retrieve it again.

<Parameters
  title="Path"
  rows="[
  { name: 'keyId', type: 'string (UUID)', required: true, description: 'The api_keys.id of the key to rotate. Must belong to the same org as the calling key.' },
]"
/>

<Parameters
  title="Headers"
  rows="[
  { name: 'Idempotency-Key', type: 'string (UUID)', required: true, description: 'Strongly recommended. Replays within 24h return the SAME new secret — without this, a network retry silently issues two rotations and leaves you with the wrong secret.' },
]"
/>

## Example [#example]

```bash
# Quarterly rotation
NEW_KEY=$(curl -s -X POST https://api.layers.com/v1/api-keys/c2037bb9-354d-4662-96b7-97a28ad6b6e1/rotate \
  -H "X-Api-Key: $LAYERS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" | jq -r .secret)

# Deploy NEW_KEY to your services, then verify:
curl https://api.layers.com/v1/whoami -H "X-Api-Key: $NEW_KEY"
```

<Response status="200" description="OK — key rotated, new secret returned once">
  ```json
  {
    "apiKey": {
      "id": "c2037bb9-354d-4662-96b7-97a28ad6b6e1",
      "organizationId": "2481fa5c-a404-44ed-a561-565392499abc",
      "name": "production-service",
      "prefix": "lp_live_01HXB7PQRSABC9KZ",
      "killSwitch": false,
      "isActive": true,
      "rotatedAt": "2026-04-20T18:14:02.187Z",
      "revokedAt": null
    },
    "secret": "lp_live_01HXB7PQRSABC9KZ_<redacted>",
    "warning": "Store this secret now. It cannot be retrieved again. Rotate the key if it's lost."
  }
  ```
</Response>

## Rotation runbook [#rotation-runbook]

1. **Call `/rotate`** with a fresh `Idempotency-Key`. Grab `secret` from the response body.
2. **Deploy the new secret** to every service that uses the old one. Do this BEFORE relying on the new key — the old secret is already dead.
3. **Verify** with `GET /v1/whoami` using the new secret. A 200 confirms end-to-end.
4. **Audit** via [`GET /v1/audit-log?eventType=api_key.rotated`](/docs/api/reference/audit-log/list) — the rotation is recorded with the calling key + target key + request id.

Idempotency is load-bearing here. If step 1 times out and you retry without the same `Idempotency-Key`, you'll mint a second rotation and your first `secret` becomes stale. Always pass an `Idempotency-Key`.

## Errors [#errors]

| Status | Code                   | When                                                 |
| ------ | ---------------------- | ---------------------------------------------------- |
| 404    | `NOT_FOUND`            | Key ID doesn't exist, or belongs to a different org. |
| 409    | `IDEMPOTENCY_CONFLICT` | Idempotency-Key reused with a different body.        |
| 422    | `VALIDATION`           | Missing `:keyId`.                                    |

## See also [#see-also]

* [Kill](/docs/api/reference/api-keys/kill) — emergency, one-way; use this when the old secret is compromised.
* [Delete](/docs/api/reference/api-keys/delete) — retire a key cleanly without replacing it.
* [API keys concept](/docs/api/concepts/api-keys) — lifecycle diagram.
* [Audit log](/docs/api/reference/audit-log/list) — `api_key.rotated` events.
