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

# API keys

> Long-lived keys for server-to-server calls — create, use, expire, revoke.

Sessions are the right shape for users — short-lived, refresh-able, revocable per device. For **server-to-server** calls (a webhook from Stripe, a cron job in another service, a CI script), you want long-lived, independently-revocable **API keys**, not 30-day session tokens.

The framework ships this in the box.

## Built in — nothing to enable

API keys are first-class and always on. There's no plugin to add and no
manifest entity to declare — the framework owns a dedicated key store
(persisted to your `PYLON_SESSION_DB` SQLite file, in-memory if unset) and
mounts the management endpoints under `/api/auth/api-keys`.

What you get:

* `/api/auth/api-keys` endpoints to create, list, and revoke keys
* Bearer-token resolution: a request with `Authorization: Bearer pk.<...>`
  resolves to the key's owner instead of looking up a session

## What's stored

The key store keeps an internal record per key (not a manifest entity):

| Field          | Notes                                                                  |
| -------------- | ---------------------------------------------------------------------- |
| `id`           | `key_<24 chars>` — used to revoke a key                                |
| `user_id`      | Owner of the key                                                       |
| `name`         | Human label                                                            |
| `prefix`       | First 16 chars of the plaintext (`pk.key_<8 id chars>`) — safe to show |
| `secret_hash`  | HMAC-SHA256 of the secret (see [Security](#security))                  |
| `scopes`       | Optional, opaque comma-separated string (your app defines the meaning) |
| `expires_at`   | Optional unix timestamp; unset = no expiry                             |
| `last_used_at` | Refreshed on every successful auth                                     |
| `created_at`   | Unix timestamp                                                         |

Only the `prefix` is shown in lists — the full key is shown **once**, at creation time, and never again.

## Create a key

```bash theme={null}
curl -X POST https://your-app/api/auth/api-keys \
  -H 'Authorization: Bearer pylon_<session>' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Stripe webhook handler",
    "scopes": "webhooks,payments:write",
    "expires_at": null
  }'
```

`scopes` is an optional free-form string and `expires_at` is an optional unix timestamp (seconds). API-key auth can't create keys — a real session is required (`403 API_KEY_AUTH_FORBIDDEN` otherwise), so a leaked key can't mint more keys.

Response (200):

```json theme={null}
{
  "key": "pk.key_a1b2c3d4....secret",
  "id": "key_a1b2c3d4...",
  "prefix": "pk.key_a1b2c3d4e",
  "name": "Stripe webhook handler",
  "scopes": "webhooks,payments:write",
  "expires_at": null,
  "created_at": 1735000000
}
```

**The `key` field is shown exactly once** — copy it now or revoke and re-create. The server stores only the hash.

## Use a key

Just like a session — `Authorization: Bearer <key>`:

```bash theme={null}
curl -X POST https://your-app/api/fn/processStripeEvent \
  -H 'Authorization: Bearer pk.key_a1b2c3d4....secret' \
  -H 'Content-Type: application/json' \
  -d '{ "event": {...} }'
```

The runtime resolves the key via constant-time hash comparison and produces an `AuthContext` with the key owner's `userId`. The key's `scopes` are **not** interpreted or enforced by pylon — see below.

## Scopes

Scopes are an **opaque, application-defined** label. Pylon stores whatever
string you pass and shows it back when you list keys, but it does **not**
parse or enforce it — there is no built-in scope vocabulary, no automatic
"read vs write" gating, and the scope string is **not** surfaced to your
function or policy code. Treat it as metadata for humans auditing the key
list, not as an access-control mechanism.

Two things actually constrain what a key can do:

* **It resolves to the key's owner `userId`.** A key can do whatever that
  user can do, subject to your entity policies.
* **It does not load the owner's roles.** `auth.hasRole(...)` is `false` for
  key-authenticated requests, so a key can't exercise role-gated policies its
  owner otherwise could.

If you need finer per-key restriction, the reliable levers are minting the
key under a narrowly-privileged service user, and checking `expires_at`.
Don't rely on the `scopes` string to enforce anything on its own.

## List keys

```bash theme={null}
curl https://your-app/api/auth/api-keys \
  -H 'Authorization: Bearer pylon_<session>'
```

Response:

```json theme={null}
[
  {
    "id": "key_a1b2c3d4...",
    "name": "Stripe webhook handler",
    "prefix": "pk.key_a1b2c3d4e",
    "scopes": "webhooks,payments:write",
    "expires_at": null,
    "last_used_at": 1735003920,
    "created_at": 1735000000
  }
]
```

The full key is **not** returned. Only the prefix and metadata.

## Rotate

There's no dedicated rotate endpoint — rotate by creating a fresh key and revoking the old one:

```bash theme={null}
# 1. Mint the replacement
curl -X POST https://your-app/api/auth/api-keys \
  -H 'Authorization: Bearer pylon_<session>' \
  -d '{ "name": "Stripe webhook handler", "scopes": "webhooks,payments:write" }'

# 2. Roll the new key out to your downstream system, then revoke the old one
curl -X DELETE https://your-app/api/auth/api-keys/key_old... \
  -H 'Authorization: Bearer pylon_<session>'
```

The old key stops working the instant it's revoked.

## Revoke

```bash theme={null}
curl -X DELETE https://your-app/api/auth/api-keys/key_a1b2c3d4... \
  -H 'Authorization: Bearer pylon_<session>'
```

Ownership is checked first — you can only revoke your own keys (`404 NOT_FOUND` otherwise). Returns `{"revoked": true}`, and any subsequent request with that key gets `401 INVALID_API_KEY`.

## Expiry

Set `expires_at` (unix seconds) to make a key auto-expire:

```bash theme={null}
curl -X POST https://your-app/api/auth/api-keys \
  -H 'Authorization: Bearer pylon_<session>' \
  -d '{
    "name": "CI deploy key",
    "scopes": "deploy",
    "expires_at": 1798761599
  }'
```

Once past `expires_at`, the key is rejected at resolution as `401 INVALID_API_KEY`. The `last_used_at` field updates on every successful request — list your keys and diff it against now to find dormant keys you can safely revoke.

## Security

* **Keys are CSPRNG-generated** — a 256-bit random secret, wire format `pk.key_<id>.<secret>`.
* **Hashed with HMAC-SHA256** server-side. Unlike passwords (Argon2id), API-key secrets are full-entropy random, so a fast keyed hash is safe here and adds no per-request latency; a slow KDF would buy nothing.
* **Constant-time comparison** — no timing leak on key resolution.
* **Shown once** — the plaintext key never touches the database; only the hash is stored.
* **Owner-scoped management** — creating, listing, and revoking keys all require a real session, never an API key.

## Common patterns

### Per-integration keys

One key per third-party integration. Stripe, SendGrid, your CI pipeline, your monitoring agent — each gets a key scoped to exactly the functions it calls.

### Per-environment keys

CI key for staging, separate key for prod. Rotate independently. If a CI build leaks the staging key, prod isn't affected.

### Time-limited keys for handoffs

A consultant needs access for two weeks? Mint a key with `expires_at` 14 days out (and enforce read-only in your functions if you tag it with a `read` scope). Auto-expires; no one needs to remember to revoke it.

### Webhook signature backup

Even with HMAC-signed webhooks, requiring an API key on top means a leaked HMAC secret alone isn't enough. Defense in depth.

## Differences from sessions

| Sessions                                          | API keys                                              |
| ------------------------------------------------- | ----------------------------------------------------- |
| 30-day default lifetime                           | Indefinite by default; explicit `expiresAt`           |
| Refresh-able                                      | Rotate-able                                           |
| Per-device tracking                               | Per-integration tracking                              |
| Cookie or bearer                                  | Bearer only                                           |
| User-bound                                        | User-bound, but typically used for service-to-service |
| Token prefix `pylon_`                             | Token prefix `pk.`                                    |
| Shown only via `/api/auth/sessions` (prefix only) | Shown once at creation, then prefix only              |

## When to use a session instead

You *can* do server-to-server auth with a long-lived session token — but you lose per-integration tracking, explicit expiry, and clean revocation. Prefer keys for any non-user caller.
