> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# API key lifecycle and rotation: grace versus immediate

> How an API key moves from active to retired — the two rotation modes, the dual-valid grace window, the oldKeyExpiresAt cutoff rule, and when to rotate versus revoke.

# API key lifecycle and rotation

An Orbit API key moves through three states — active, grace-rotated, and revoked or expired — and two rotation modes move it across the boundaries. This page defines the states, the difference between grace and immediate rotation, how the cutoff on the old key is computed, and when to rotate versus revoke. For the credential classes and resolution rule, start with the [authentication and session model](/concepts/authentication-model).

## Section 1 — The lifecycle at a glance

Every API key is born in one of two states and ends in one of two states:

```mermaid theme={null}
stateDiagram-v2
  state "Active (unexpired)" as A
  state "Grace-rotated (old key still valid until old_key_expires_at)" as G
  state "Revoked / expired" as R
  [*] --> A : key created
  A --> G : rotate with mode "grace"
  A --> R : rotate with mode "immediate"
  A --> R : revoke, scheduled revoke, or expires_at passes
  G --> R : old_key_expires_at passes
```

* **Active.** The key authenticates requests. `active` is true and neither a grace window nor a scheduled cutoff constrains it.
* **Grace-rotated.** A rotation ran with the default grace mode: a replacement key was minted and the old key keeps authenticating until its computed cutoff (`old_key_expires_at`). Both keys are valid simultaneously during this window.
* **Revoked / expired.** The key no longer authenticates — either because an immediate rotation or an explicit revoke killed it, or because its expiry instant (`expires_at` or `old_key_expires_at`) passed.

A key does not need rotation to reach the end state: `POST /api/v1/settings/api-keys/:id/revoke`, a scheduled revocation cutoff (`revoke_at`), or a customer-set `expires_at` all terminate it directly.

## Section 2 — The two rotation modes

`POST /api/v1/settings/api-keys/:id/rotate` mints a replacement key and retires the old one. The `mode` field picks which retirement path the old key takes:

| Mode              | What happens to the old key                                                                                         | When to use it                                          |
| ----------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `grace` (default) | Stays valid for `grace_period_hours` (default 24, clamped to 1–168 hours). Both keys authenticate until the cutoff. | Routine rotation — swap integrations with no downtime.  |
| `immediate`       | Revoked in the same transaction. It stops authenticating the instant the request commits.                           | Leaked or compromised key — the incident-response path. |

Two properties of the mode resolution are worth knowing:

* **Grace is the floor for routine rotations.** Omitting `mode`, sending `mode: "grace"`, or sending an unrecognized value all resolve to the grace path. The grace window is never zero — it is clamped to a minimum of 1 hour — so a routine rotation cannot become an accidental panic-revoke through a typo.
* **Immediate is an explicit, separate action.** It is not "grace with zero hours"; the grace path enforces the 1-hour minimum precisely so the dual-validity guarantee holds for routine rotations. Sending `mode: "immediate"` is the only way the old key dies in the same transaction, and on that path `grace_period_hours` is ignored.

The two modes also leave different audit trails: grace rotation writes an `api_key.rotated` entry, immediate rotation writes `api_key.revoked_immediate`, so a compliance review can separate routine rotations from panic-revokes without reading request payloads.

## Section 3 — The dual-key grace window

In grace mode the old and new keys validate at the same time until `old_key_expires_at`. That overlap is the entire point: you rotate, update every integration to the new key on its own schedule, and the old key keeps answering requests until the window closes.

This is deliberately not a "grace = 0" behavior. The 1-hour floor preserves the overlap for every routine rotation — a zero-length window is only reachable through the explicit `immediate` mode, which exists for keys that must die now.

Plan the cutover against the cutoff returned in the rotation response. If the window ends before you finish migrating, every request still carrying the old key starts failing `401` at that instant.

## Section 4 — How `old_key_expires_at` is computed

Rotation never silently extends the life of the old key. The cutoff is the earlier of two instants:

1. The old key's pre-existing `expires_at`, if one was set at creation.
2. The new grace window (rotation time plus `grace_period_hours`).

If you gave the key a 2-hour expiry and then rotate it with the default 24-hour grace window, the old key dies after 2 hours, not 24. For `immediate` mode the cutoff is the rotation instant itself. The resolved cutoff comes back as `old_key_expires_at` in the response so the dashboard (and your runbook) can show "old key valid until …" without a second call.

## Section 5 — Provenance and audit capture

Every rotation records the old-to-new pair without ever writing the secret to the audit log:

* The old key's `lastFourChars` suffix is captured before the rotation runs — after the rotation, the key list shows the *new* suffix, so without this look-back the "rotated from …1234 to …5678" provenance would be lost.
* The audit entry carries the key id, the replacement key id, the key label, both suffixes, the resolved mode, and (for grace) the requested window. The plaintext of the new key never appears there.

The new key's `key` field — its plaintext — is returned exactly once, in the rotation response, with `Cache-Control: no-store`. Store it in your secret manager immediately; later reads only ever show the masked prefix and suffix.

## Section 6 — Rotate versus revoke

| Situation                                                                  | Action                                                                                                                                                                                                      |
| -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Routine hygiene, a staff change, or a scheduled credential refresh         | Rotate in grace mode; migrate integrations inside the window.                                                                                                                                               |
| A key leaked, was committed to a repo, or showed up in someone else's logs | Rotate with `mode: "immediate"` — replacement minted and old key killed atomically, so your integrations keep working on the new key. Or revoke outright if nothing should authenticate until you re-issue. |
| The key is genuinely retired — no replacement needed                       | Revoke (`POST /api/v1/settings/api-keys/:id/revoke`), or a bulk revoke for a batch.                                                                                                                         |
| You want a known future cut-off without action right now                   | Schedule a revocation (`POST /api/v1/settings/api-keys/:id/schedule-revoke`); the key stays valid until `revoke_at` and you can cancel before the cutoff.                                                   |

One billing note for the leak case: revoking a leaked key stops the meter at the revoke timestamp — usage billed into your account ends there regardless of what the leaked key does afterward, so revoking is always the right first move even before you investigate.

## Section 7 — A guided cutover

1. **List keys first** so you target the right id: `GET /api/v1/settings/api-keys` returns labels, prefixes, suffixes, and expiry metadata — secrets are never listed.
2. **Rotate with grace.**

```bash theme={null}
curl -X POST https://orbit.devotel.io/api/v1/settings/api-keys/key_01H8X/rotate \
  -H "X-API-Key: dv_live_sk_…" \
  -H "Content-Type: application/json" \
  -d '{"grace_period_hours": 24}'
```

```json theme={null}
{
  "data": {
    "id": "key_01H9Q2",
    "key": "dv_live_sk_9f3c…new-plaintext…",
    "keyPrefix": "dv_live_sk_",
    "lastFourChars": "8f2a",
    "oldLastFourChars": "5b7d",
    "name": "payments-worker",
    "type": "secret",
    "oldKeyExpiresAt": "2026-09-05T10:24:00.000Z",
    "rotationMode": "grace"
  },
  "meta": { "requestId": "req_9c2fa1" }
}
```

3. **Copy `key` into your secret manager now** — this response is the only place the plaintext ever appears.
4. **Deploy the new key** to every integration. The old key keeps working against the same scopes until `oldKeyExpiresAt`, so you can roll service by service.
5. **Confirm the cutover.** After `oldKeyExpiresAt` passes, any request still carrying the old key fails `401` — grep your access logs for the old suffix (`…5b7d`) ahead of the cutoff if you want zero surprises.

Owner, admin, or developer roles can rotate. For a leaked key, the same call with `"mode": "immediate"` mints the replacement and revokes the old key in one transaction — do the copy step before you send it.
