> Markdown version of https://authpi.com/docs/guides/api-keys/ — fetch the complete AuthPI docs index at https://authpi.com/llms.txt to discover all available pages.

# Issue and manage API keys

Create, scope, rotate, and revoke organization API keys for the AuthPI Core API — covering scopes, IP allowlists, expiry, and one-time secrets.

API keys are organization-scoped credentials for calling the AuthPI Core API from backend services, CI/CD pipelines, and scripts — anywhere there is no user session. Every key is an **id + secret pair**: the id (`key_...`) identifies the key, the secret authenticates it. AuthPI stores only a hash of the secret, so the plaintext is returned exactly once, at creation.

Account-level keys live in your account's own organization and authenticate against your account on the Core API. You can also mint keys inside one of your issuer's organizations (`POST /v1/accounts/{account_id}/issuers/{issuer_id}/organizations/{org_id}/api-keys`) — those authenticate as that organization rather than your account. This guide focuses on account-level keys.

## Create a key

Create keys with the account-level endpoint. `name` and `restrictions` are required; scopes default to an empty list, which means the key authenticates but is denied at every scope-gated route (authorization is deny-by-default), so set scopes up front:

```bash
curl -X POST "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/api-keys" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ci-deploy",
    "restrictions": {
      "scopes": ["issuers.users:read", "webhooks:manage"]
    },
    "expires_at": 1788998400000
  }'
```

The `201` response is the only time you will ever see the secret:

```json
{
  "data": {
    "id": "key_4f8a2b9c0d1e4f5a8b7c6d5e4f3a2b1c",
    "type": "api_key",
    "org_id": "org_7k2m9x4p1q8w5e3r6t0y2u4i7",
    "name": "ci-deploy",
    "status": "active",
    "restrictions": { "scopes": ["issuers.users:read", "webhooks:manage"] },
    "expires_at": 1788998400000,
    "secret": { "algorithm": "sha256", "hint": "K2J9xQ4z" },
    "created_at": 1781222400000,
    "updated_at": 1781222400000,
    "secret_plain": "Qm3kZ8vN5tR2wY7xJ4cF9bL6hD1sG0aEuPK2J9xQ4z"
  }
}
```

Store `secret_plain` in a secret manager immediately — it cannot be retrieved again. Afterwards, only the `secret.hint` (by default the last 8 characters of the secret) remains visible, so you can match a key in the dashboard to the credential a service is holding. You can supply your own `secret` (12–200 characters) and `secret_hint` in the create request instead of letting the server generate them.

Accounts are limited to 100 API keys; the create endpoint returns `429 too_many_api_keys` beyond that.

## Authenticate with a key

API keys use **HTTP Basic auth**: the key id is the username, the secret is the password. Never send an API key as a `Bearer` token — the Bearer scheme is reserved for JWTs, and a key sent that way fails JWT verification with a 401.

```bash
curl "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/webhooks" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

With the Admin SDK, pass the pair as `apiKey`. The account id is optional — when omitted, the SDK resolves it once via `GET /v1/me` and caches it (an API key always maps to exactly one account):

```typescript
import { AuthPIAdmin } from "@authpi/admin";

const admin = new AuthPIAdmin({
  apiKey: { id: process.env.AUTHPI_KEY_ID!, secret: process.env.AUTHPI_KEY_SECRET! },
});

const issuers = await admin.issuers.list({ limit: 10 });
```

Credential failures always return `401` with a specific reason — `Invalid API key credentials`, `API key has expired`, `API key is blocked`, `API key has been revoked`, or `IP address not allowed for this API key`. A `403` means the key authenticated but its scopes don't allow the request.

### Which account does this key belong to?

If you have a key but not its account ID, ask the API — `GET /v1/me` returns the caller's identity and the verified accounts the credential can act on (the equivalent of AWS STS `GetCallerIdentity`):

```bash
curl "https://api.authpi.com/v1/me" -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

```json
{
  "data": {
    "type": "api_key",
    "key_id": "key_8a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d",
    "issuer_id": "i_x2v9qkw4mt7rne",
    "accounts": [
      {
        "account_id": "acc_7k2m9x4p1q8w5e3r6t0y2u4i7",
        "org_id": "org_7k2m9x4p1q8w5e3r6t0y2u4i7",
        "scopes": ["*:**"]
      }
    ]
  }
}
```

`accounts` is always an array (one entry for an API key), so the same resolution logic works for every credential type. It's also the fastest way to debug a key: one call shows what the key is and what it can reach.

## Restrict what a key can do

A key's `restrictions.scopes` become its authorization context: on every request, the Core API evaluates the route's required action (`read`, `write`, `delete`, or `manage`) against the key's scopes, and denies by default. Scopes use the grammar `resource[.subresource]:action`:

| Scope | Grants |
|-------|--------|
| `issuers:read` | Read issuers |
| `issuers.users:write` | Write users in any issuer — write only; pair it with `issuers.users:read` |
| `webhooks:manage` | Full control of webhooks (`manage` implies read, write, and delete) |
| `notes:*` | All four actions on notes |
| `*:**` | Full access to everything |

Actions do not imply each other (except `manage`, which covers all of them), so a write-only key cannot read what it writes. Deny-by-default applies to reads as much as writes: every `GET` requires the matching `:read` scope (or `:manage` on that resource), so a key holding only `webhooks:manage` can read webhooks but gets a `403` listing users, sessions, or invitations. Scopes that don't match this grammar are rejected with a `400` at creation time. When multiple scopes match a request, the most specific one wins. There is no deny syntax — to exclude a resource, grant the others explicitly instead of starting from `*:**`.

**IP allowlist.** `restrictions.ip_allowlist` limits where a key can be used from. Entries are currently matched **exactly** against the caller's IP — list individual IPv4/IPv6 addresses. CIDR notation is accepted by the API but not yet evaluated as a range, so a CIDR entry will not match any caller. An empty allowlist means all IPs are allowed.

**Expiry.** Set `expires_at` (Unix milliseconds) so forgotten keys die on their own. Once the timestamp passes, the key's status flips to `expired` and authentication fails. You can renew an expired key by `PATCH`ing a future `expires_at` together with `"status": "active"`.

Update restrictions any time with `PATCH /v1/accounts/{account_id}/api-keys/{key_id}` — changes take effect on the next request.

## Rotate the secret

Rotation generates a new secret without recreating the key:

```bash
curl -X POST "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/api-keys/key_4f8a2b9c0d1e4f5a8b7c6d5e4f3a2b1c/rotate" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

```json
{
  "data": {
    "secret_plain": "xT7pW2rH9mC4vK1nB6dJ3gQ8sZ5fL0aYeR4uM9kP2w"
  }
}
```

As at creation, the new secret is returned once. The **old secret remains valid for 15 minutes** after rotation, so you can roll the new credential out to your services without a hard cutover — after 15 minutes only the new secret works. You may pass `new_secret` (12–200 characters) and `hint` in the request body to bring your own secret. The SDK equivalent is `admin.apiKeys.rotate("key_...")`.

Rotate on a schedule (90 days is a common policy) and immediately whenever a secret may have leaked. Revoked keys cannot be rotated.

## Block, unblock, or revoke

Two distinct kill switches:

- **Block** (`POST .../api-keys/{key_id}/block`) is **reversible**. Authentication fails immediately, but `POST .../unblock` restores the key to `active`. Use it while investigating suspicious activity or during maintenance.
- **Revoke** (`POST .../api-keys/{key_id}/revoke`) is **terminal**. The key can never be reactivated, updated, rotated, or unblocked again; its record is retained for 31 days for auditing and then permanently deleted. `DELETE /v1/accounts/{account_id}/api-keys/{key_id}` is equivalent to revoking.

Both accept an optional audit body:

```bash
curl -X POST "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/api-keys/key_4f8a2b9c0d1e4f5a8b7c6d5e4f3a2b1c/revoke" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "by": "security-team", "reason": "credential found in build logs" }'
```

Rule of thumb: block when you might be wrong, revoke when you know you're done.

## List and audit keys

List keys with pagination and a `status` filter (`active`, `blocked`, `revoked`, `expired`, `suspended`):

```bash
curl "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/api-keys?status=active&limit=50" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
```

Each entry includes the key's name, status, scopes, `expires_at`, and `secret_hint` — enough to audit what exists and what each key can do, without ever exposing a secret. Key lifecycle changes also emit events (`api-key.created`, `api-key.updated`, `api-key.blocked`, `api-key.unblocked`, `api-key.deleted`) that you can subscribe to with [webhooks](/docs/guides/webhooks/).

## Plan for the key lifecycle

- **Secrets are shown once.** Creation and rotation are the only moments the plaintext exists outside your infrastructure. If a secret is lost, rotate — there is no recovery.
- **Keys die with their organization.** Deleting an organization first revokes every API key it owns — the deletion only completes once all keys are revoked, and each revocation emits `api-key.deleted`. Decommission the services using those keys before deleting the org, or they'll start failing with `revoked`.
- **One key per service.** Mint a separate, minimally-scoped key for each consumer so a rotation or revocation never takes down unrelated systems, and audit trails stay attributable.
- **Prefer expiring keys in production.** A key with `expires_at` fails safe; a forgotten eternal key is a standing liability.

## Next steps

- Decide between keys, clients, and agents in [Machine-to-machine auth](/docs/guides/m2m-auth/)
- Browse every endpoint in the [API keys reference](/docs/reference/core-api/api-keys/)
- Check [rate limits](/docs/reference/rate-limits/) that apply to key-authenticated requests
- Understand [organizations](/docs/concepts/organizations/), the entity every key belongs to