Guides

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.

Last updated 2026-06-13

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:

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:

{
  "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.

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):

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):

curl "https://api.authpi.com/v1/me" -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
{
  "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:

ScopeGrants
issuers:readRead issuers
issuers.users:writeWrite users in any issuer — write only; pair it with issuers.users:read
webhooks:manageFull 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 PATCHing 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:

curl -X POST "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/api-keys/key_4f8a2b9c0d1e4f5a8b7c6d5e4f3a2b1c/rotate" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET"
{
  "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:

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):

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.

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