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.
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,
Widgetlives undercomponents.schemas.Widget; the two emptyschemaslines above are placeholders so the fragment reads end to end. Replace them with yourcomponents.schemasblock.
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.
Related
- Security Schemes & OAuth Flows — parent section overview.
- OAuth2 Client Credentials in OpenAPI 3.1 — token-based machine-to-machine auth.
- OAuth2 Authorization Code + PKCE in OpenAPI 3.1 — browser and mobile user auth.
- OpenAPI & AsyncAPI Schema Authoring — the full spec-authoring guide.