---
title: "Add API Key Authentication"
url: "https://effect-auth.itsbroly.com/recipes/add-api-key-authentication/"
description: "Create, manage, and verify scoped machine credentials."
---



Use the built-in API-key management routes for a signed-in human to create, list, and revoke credentials. Verify those credentials separately at your application boundary.

```text
operator session                    machine request
      |                                   |
      v                                   v
/auth/api-keys                    Authorization: Bearer <prefix>.<secret>
 create | list | revoke                    |
      |                                   v
      +---- durable ApiKeyStore <---- prefix lookup -> hash comparison
                                               |
                                               v
                                  scope + app ownership checks -> handler
```

## 1. Wire management and verification [#1-wire-management-and-verification]

The focused `ApiKeyHttpApiLive` preset exposes `POST /auth/api-keys`, `GET /auth/api-keys`, and `POST /auth/api-keys/revoke`. If these routes must live in your own API, bind `ApiKeyHttpOperations.create`, `.list`, and `.revoke` instead. Both choices require the same domain layers:

```ts
import {
  ApiKeyManagementLive,
  ApiKeysLive,
  ApiKeyVerificationLive
} from "@effect-auth/core/ApiKey";
import {
  ApiKeyHttpApiLive,
  ApiKeyHttpOperationsLive
} from "@effect-auth/core/HttpApi";
import { Layer } from "effect";

const ApiKeyDomainLive = Layer.merge(
  ApiKeyManagementLive.pipe(Layer.provide(ApiKeysLive)),
  ApiKeyVerificationLive.pipe(Layer.provide(ApiKeysLive))
).pipe(
  Layer.provide(AppApiKeyStoreLive), // your durable ApiKeyStore layer
  Layer.provide(AppCryptoLive)
);
```

Provide `ApiKeyHttpApiLive` (or `ApiKeyHttpOperationsLive`) with `ApiKeyDomainLive` and your existing session, cookie, HTTP, and security layers. The management operations authenticate the operator from the session and always derive `userId` server-side; a caller cannot manage another user's keys.

Apply `migrations/0008_auth_api_key.sql` when using the SQLite storage stack, or implement the public `ApiKeyStore` contract in another database. The table indexes a unique random prefix for lookup and stores `secret_hash`, never the secret. `ApiKeyStoreMemoryLive` is suitable for tests, not production. See [Storage](/reference/storage/) and [HTTP Operations](/reference/http-operations/).

## 2. Create, display once, list, and revoke [#2-create-display-once-list-and-revoke]

The public client uses the preset paths and the operator's session cookie:

```ts
import { createApiKeyClient } from "@effect-auth/core/Client";

const apiKeys = createApiKeyClient({ baseUrl: "https://auth.example.com" });

const created = await apiKeys.keys.create({
  scopes: ["deployments:write"],
  expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
  metadata: { label: "production deployer", applicationId: "app_123" }
});

showSecretOnce(created.secret); // app UI: require immediate copy/download
const { keys } = await apiKeys.keys.list();
await apiKeys.keys.revoke({ keyId: created.key.keyId, reason: "rotated" });
```

`create` is the only response containing the full secret. Listing returns the key ID, prefix, scopes, timestamps, and metadata, so losing the secret requires creating a replacement. Never log, persist in browser storage, email, or redisplay it. Treat metadata as descriptive only; enforce application ownership from trusted records, not a client-editable label.

## 3. Protect an API [#3-protect-an-api]

Read the bearer token and require scopes with the public machine-auth helper:

```ts
import { ApiKeyVerification } from "@effect-auth/core/ApiKey";
import { verifyMachineApiKey } from "@effect-auth/core/MachineAuth";
import { Effect } from "effect";

const deploy = (request: Request) => Effect.gen(function* () {
  const verification = yield* ApiKeyVerification;
  const actor = yield* verifyMachineApiKey({
    headers: request.headers,
    apiKeyVerification: verification,
    requiredScopes: ["deployments:write"],
    verificationMetadata: { route: "deploy" }
  });

  yield* requireApplicationAccess(actor.userId, "app_123"); // app-owned
  return yield* deployments.start({ applicationId: "app_123", actor });
});
```

Verification rejects malformed, unknown, revoked, expired, incorrectly hashed, and under-scoped keys, and updates `lastUsedAt`. Map missing/invalid credentials to `401`, missing scope or ownership to `403`, and infrastructure failures to a sanitized `500`. The final ownership check belongs to your application and should be repeated transactionally for sensitive writes. See [Security Policies](/concepts/security-policies/) and [Protect an API with app-owned guards](/guides/app-owned-guards/).

## Test and operate [#test-and-operate]

An integration test should create a key, capture its one-time secret, verify a scoped request succeeds, assert a missing scope fails, list without exposing the secret/hash, revoke it, then assert the same bearer token fails. Use `makeApiKeyStoreMemory()` for unit tests and run the storage contract against the production adapter.

In production, rate-limit both management and application endpoints using durable counters, audit create/list/revoke and accepted/denied use by key ID and prefix, and never record bearer values. Set expirations, grant least-privilege scopes, rotate by creating and deploying a replacement before revoking the old key, protect operator routes with origin/CSRF policy, and alert on unusual failures or use.

