# Expiration Policies

Expiration is about automatic key death at a predetermined time. For proactive key replacement before expiration, where a new key is issued and the old one is phased out on a schedule, see [Key Rotation](/security/key-rotation/).

## The Risk of Never-Expiring Keys

A key that never expires is a key that works forever, for anyone who has it. Over time, the probability that a long-lived key has been leaked, logged, or forgotten approaches certainty. Teams change, employees leave, integrations get deprecated, but the key keeps working.

Never-expiring keys also violate the principle of defense in depth. [Rotation](/docs/security/key-rotation), [scoping](/docs/security/scoping-and-permissions), and [rate limiting](/docs/security/rate-limiting) all reduce risk, but expiration provides a hard backstop: even if every other control fails, the key eventually stops working.

## Choosing an Expiration Window

The right expiration period depends on the sensitivity of the API and the operational burden on consumers:

| Use Case | Suggested Expiration | Rationale |
|---|---|---|
| Internal service-to-service | 30-90 days | Automated rotation makes short lifetimes practical |
| Third-party integrations | 6-12 months | Balances security with partner convenience |
| CI/CD pipeline tokens | 1-7 days | Short-lived, created on demand per run |
| One-time data exports | Hours | Scoped to a single task, then discarded |

There is no universal answer, but the default should always be "this key expires" rather than "this key lives forever." Make non-expiring keys an explicit opt-in that requires justification.

## Short-Lived vs. Long-Lived Keys

**Short-lived keys** (minutes to days) are ideal when automation handles issuance and renewal. CI/CD pipelines, serverless functions, and machine-to-machine flows can request a fresh key for each job. The tradeoff is more infrastructure to manage token issuance.

**Long-lived keys** (months to years) make sense when a human is involved in the configuration. A developer pasting a key into a third-party dashboard shouldn't need to update it every week. The tradeoff is a wider exposure window if the key [leaks](/docs/security/leak-detection).

When in doubt, start with shorter expiration and extend it only if consumer feedback demands it.

## Renewal Flows

Make renewal as frictionless as possible:

1. **Self-service renewal**: Consumers visit the developer portal or call a renewal endpoint before the key expires. The new key is returned immediately, and the old key enters a grace period.
2. **Automatic renewal**: For internal services, a secrets manager or sidecar process requests a new key before the current one expires, without human intervention.
3. **Prompted renewal**: The API returns a warning header as the key approaches expiration, prompting the consumer to act.

```http
HTTP/1.1 200 OK
X-Api-Key-Expires: 2026-05-01T00:00:00Z
X-Api-Key-Expires-In: 604800
```

## Automated Expiration Warnings

Consumers should never be surprised by an expired key. Build a notification timeline:

| Timeframe | Action |
|---|---|
| 30 days before expiration | Email notification + dashboard banner |
| 7 days before expiration | Email reminder + webhook event |
| 1 day before expiration | Urgent email + API response header warning |
| On expiration | Key returns `401 Unauthorized` with a clear error message |

```json
{
  "error": "key_expired",
  "message": "This API key expired on 2026-05-01. Generate a new key at https://dashboard.example.com/keys."
}
```

Include a direct link to the key management page in every expiration message. The fewer steps between "my key expired" and "I have a new key," the less downtime the consumer experiences.

## Enforcement

When a key expires, it should immediately return `401 Unauthorized`. Do not allow a soft grace period after the stated expiration date, as that undermines trust in the expiration system. The grace period belongs *before* expiration (during rotation), not after.

Store the `expires_at` timestamp alongside the key hash and check it on every request. This check is cheap and should happen early in your middleware pipeline, before any business logic executes.

```javascript
async function checkKeyExpiration(req, res, next) {
  const { expires_at } = req.apiKeyRecord;

  if (expires_at && new Date(expires_at) <= new Date()) {
    return res.status(401).json({
      error: "key_expired",
      message: `This API key expired on ${expires_at}. Generate a new key at https://dashboard.example.com/keys.`,
    });
  }

  // Optionally warn if expiration is approaching
  if (expires_at) {
    const msRemaining = new Date(expires_at) - new Date();
    const sevenDays = 7 * 24 * 60 * 60 * 1000;
    if (msRemaining < sevenDays) {
      res.setHeader("X-Api-Key-Expires", expires_at);
      res.setHeader("X-Api-Key-Expires-In", String(Math.floor(msRemaining / 1000)));
    }
  }

  next();
}
```
