# API keys

> For a known integration - a partner, a batch, a server with no user - and not for an agent (which gets an OAuth token, Agents and MCP). A key is bound to one…

Guide - Since version 0.1.0 - Flexway v1.6.3 - https://flexway.io/en/docs/operations-api-keys/

> Operations guide: configure, deploy and observe the gateway. What the capability does, product side: [product guide](https://flexway.io/en/docs/product-overview/); calling it: [developer guide](https://flexway.io/en/docs/developer-overview/).

For a **known integration** - a partner, a batch, a server with no user - and not for an agent
(which gets an OAuth token, [Agents and MCP](https://flexway.io/en/docs/operations-agents-mcp/)). A key is bound to **one tenant**, to **named operations** and to
an **end date**; it **never** reaches the upstream.

**Command** - create a key:

```sh
docker run --rm -it ghcr.io/flexwiz/flexway:<version> api-key   # or the binary: flexway api-key
```

The command **asks** for the id, the tenant, the operations and the lifetime (90 days by default),
then prints the key **once** (`fwk_` + 43 characters, 256 random bits) and the entry to add to the
store - its **SHA-256 digest**, never the key:

```yaml
# api-keys.yaml - in a cluster, a Secret synchronised by ESO, mounted read-only
keys:
  - id: acme-billing-2026-10
    hash: sha256:9f2c...
    tenant: acme
    operations: [listInvoices, getInvoice]
    expires_at: 2027-01-01T00:00:00Z
```

The configuration names the store; the contract says **where** a key is accepted, with an `apiKey`
alternative next to the token's:

```yaml
# flexway.yaml
auth:
  api_keys: { store: "file:./api-keys.yaml" }
```

```yaml
# contract
components:
  securitySchemes:
    key: { type: apiKey, in: header, name: Authorization }   # always Authorization: ApiKey <key>
paths:
  /invoices:
    get:
      operationId: listInvoices
      security: [ { customers: [invoices:read] }, { key: [] } ]   # a token, or a bound key
```

**Check**:

| Call | Response |
| --- | --- |
| `Authorization: ApiKey <key>` on a bound operation | relayed; the upstream gets the key's `X-Tenant`, **without** the key |
| A valid key, an operation it does not name | `403` |
| A key on an operation with no `apiKey` alternative | `401`, `WWW-Authenticate: Bearer` |
| An unknown or expired key, or one without the `fwk_` prefix | `401`, `WWW-Authenticate: ApiKey`; the log names an expired key by its `id`, never the key |
| A key in another header (`X-Api-Key`) or in the URL | ignored; an `apiKey` scheme outside `Authorization` is **refused at load** |

> **Lifecycle**: the store is re-read at **every reload** ([Changing the configuration without a restart](https://flexway.io/en/docs/operations-live-config/)). Issue = add an entry; **revoke**
> = remove it; **rotate** = two entries while the integration switches, then remove the old one. An
> entry naming an operation no contract declares is refused at load, like a policy. Rate limits count
> **per key** (`apikey:<id>`).
