Documenting API Key Auth in OpenAPI 3.1

API keys are the simplest authentication mechanism to document, yet they are the most frequently mis-specified: keys placed in the wrong location, schemes defined but never referenced, and public endpoints that silently inherit a global key requirement. This guide is part of Security Schemes & OAuth Flows within the broader OpenAPI & AsyncAPI Schema Authoring section, and it shows exactly how to model apiKey schemes — in a header, query parameter, or cookie — and how to apply them globally, per operation, and in combination.

API key locations in an OpenAPI 3.1 request A client sends an API key to a server in one of three locations: an HTTP header, a query parameter, or a cookie, each mapped to an apiKey security scheme. Client API server header: X-API-Key (in: header) query: ?api_key=… (in: query) cookie: session=… (in: cookie)

Problem & Context

A typical pre-fix spec looks correct but renders no auth UI and generates no security middleware. The scheme is buried under components and never wired into a security requirement:

# BEFORE — scheme defined but never applied
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
# no root-level `security`, no operation-level `security` anywhere

The result: Swagger UI shows no Authorize button, Redoc marks every endpoint as public, and generated SDKs omit the header entirely. The apiKey type itself has three valid in values — header, query, and cookie — and each behaves differently for caching, logging, and CORS. Getting both the definition and the application right is the whole task. Unlike an OAuth2 client credentials flow, an apiKey scheme has no scopes, so authorization granularity comes entirely from which scheme you require on which operation.

Step-by-Step Solution

1. Declare the apiKey scheme

Add the scheme under components.securitySchemes. The name field is the exact parameter name the client must send, and in selects the location:

components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey      # one of: apiKey | http | oauth2 | openIdConnect | mutualTLS
      in: header        # header | query | cookie
      name: X-API-Key   # exact header/param/cookie name the caller sends
      description: Project-scoped key issued in the dashboard.

Expected behavior: a 3.1-aware tool now lists apiKeyAuth as an available scheme. It still will not enforce it until you reference the name in a security array.

2. Apply the key globally

Add a root-level security array so every operation inherits the requirement. For an apiKey scheme the value array is always empty ([]) — that array only carries scopes, which apiKey does not have:

security:
  - apiKeyAuth: []   # [] is mandatory; apiKey has no scopes

Expected behavior: Swagger UI renders a single Authorize button, and every operation without its own security block requires X-API-Key.

3. Override per operation

Operation-level security fully replaces (does not merge with) the global one. Use it to expose a public endpoint or to require a different key:

paths:
  /health:
    get:
      summary: Liveness probe
      operationId: getHealth
      security: []       # [] here = explicitly public, overrides global
      responses:
        '200': { description: OK }
  /admin/rotate-keys:
    post:
      summary: Rotate tenant keys
      operationId: rotateKeys
      security:
        - adminKeyAuth: []   # requires the admin key, not the default key
      responses:
        '202': { description: Accepted }

Expected behavior: GET /health is callable with no key; POST /admin/rotate-keys rejects the default X-API-Key and demands the admin header.

4. Combine multiple keys (AND vs OR)

The structure of the security array encodes boolean logic. Separate objects are OR (any one satisfies). Multiple entries inside one object are AND (all required together):

# OR — caller may use the API key OR a bearer token
security:
  - apiKeyAuth: []
  - bearerAuth: []
# AND — caller must send BOTH an app id and a secret key
security:
  - appIdAuth: []
    apiKeyAuth: []

Expected behavior: the OR form lets clients authenticate with either credential; the AND form rejects a request missing either header. This AND form is the canonical way to model the common “app id + secret” pattern with two apiKey schemes.

5. Validate the spec

Lint to confirm every requirement resolves to a defined scheme:

npx @redocly/cli@v2 lint openapi.yaml

Expected output on a clean spec:

validating openapi.yaml...
openapi.yaml: validated in 41ms

Woohoo! Your API description is valid. 🎉

If you reference an undefined scheme name, Redocly reports security-defined failures pointing at the offending operation.

Complete Working Example

A single self-contained openapi.yaml fragment showing a header key applied globally, a public endpoint, an admin-only endpoint with a second key, and an OR alternative for a query key:

# openapi.yaml — OpenAPI 3.1.0, validated with @redocly/cli@v2
openapi: 3.1.0
info:
  title: Widgets API
  version: 1.4.0
servers:
  - url: https://api.example.com/v1
components:
  securitySchemes:
    apiKeyAuth:          # default project key, sent as a header
      type: apiKey
      in: header
      name: X-API-Key
      description: Project key from the dashboard. Keep secret.
    legacyQueryAuth:     # legacy callers that cannot set headers
      type: apiKey
      in: query
      name: api_key
      description: Deprecated. Appears in URLs/logs  migrate to X-API-Key.
    adminKeyAuth:        # elevated key for tenant administration
      type: apiKey
      in: header
      name: X-Admin-Key
      description: Admin key with key-rotation privileges.
# Global default: every operation needs the project key OR the legacy query key
security:
  - apiKeyAuth: []
  - legacyQueryAuth: []
paths:
  /widgets:
    get:
      summary: List widgets
      operationId: listWidgets
      # inherits the global security requirement
      responses:
        '200':
          description: A page of widgets
          content:
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/Widget' }
        '401':
          description: Missing or invalid API key
  /health:
    get:
      summary: Liveness probe
      operationId: getHealth
      security: []         # explicitly public
      responses:
        '200': { description: OK }
  /admin/keys/rotate:
    post:
      summary: Rotate all project keys
      operationId: rotateKeys
      security:
        - adminKeyAuth: [] # admin key only; default key is rejected
      responses:
        '202': { description: Rotation queued }
        '403': { description: Admin key required }
schemas: {}   # define Widget under components.schemas in a real spec
components/schemas: {}

Note: in a real file, Widget lives under components.schemas.Widget; the two empty schemas lines above are placeholders so the fragment reads end to end. Replace them with your components.schemas block.

Gotchas & Edge Cases

Cookie keys collide with CORS and credentials. A scheme with in: cookie requires browsers to send credentials, which means the server must return Access-Control-Allow-Credentials: true and a specific (non-wildcard) Access-Control-Allow-Origin. Swagger UI also will not attach cookie keys to cross-origin try-it-out requests unless requestInterceptor is customized. Document this constraint or callers will see silent 401s in the browser.

Empty array vs missing security mean different things. security: [] on an operation makes it explicitly public. Omitting security on an operation makes it inherit the root-level requirement. Writing security: with no value is invalid YAML-to-schema and trips security-defined linting — always write the [].

Query keys leak into logs and proxies. A key in the query string lands in access logs, browser history, referer headers, and CDN cache keys. If you must document a query key (legacy webhooks, image URLs), mark it deprecated in the description and provide a header alternative via the OR form, exactly as the example does with legacyQueryAuth.

FAQ

Where should I put an API key — header, query, or cookie?

Prefer a header (in: header) because it stays out of URLs, logs, and browser history. Use a query key only for legacy or webhook callers that cannot set headers, and a cookie key only for browser sessions on the same site.

How do I document an endpoint that needs two keys at once?

Put both scheme names in a single object inside the security array — for example one entry listing apiKeyAuth and appIdAuth together. Entries in the same object are combined with AND, so the caller must send both.

Why does Swagger UI not show an Authorize button for my API key?

The button only appears when a securitySchemes entry is referenced by a root-level or operation-level security array. Defining the scheme under components alone is not enough; you must also reference its name in a security requirement.