> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axon402.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Create API Key

> Server-side issuance of a database-backed AXON API key

Mint a new `axon_sk_*` API key for an organization. The server generates the plaintext, HMAC-hashes it with the `AXON_API_KEY_HASH_PEPPER` (the same hash function `verifyDatabaseApiKey` uses on the read side), stores only the hash in `api_keys`, and returns the plaintext exactly once.

> [!NOTE]
>
> This route replaces the dashboard's previous client-side hashing path, which was broken because the dashboard and the verifier used different hash functions. All new keys — dashboard-issued or CLI-issued — now flow through `POST /v1/api-keys` so they are verifiable on the read side.

## Authentication

This is an operator-surface route. Accepted callers:

* A Supabase JWT with `owner` or `admin` membership on `organization_id`
* An org-wide tenant API key with write scope on the same organization
* A wallet-bound tenant API key with write scope, but only when minting another key for that same wallet
* The static operator key

```http theme={null}
POST /v1/api-keys
Authorization: Bearer <jwt-or-admin-api-key>
Content-Type: application/json
```

## Request Body

| Field | Type | Required | Description |
| :- | :- | :- | :- |
| `organization_id` | `string (uuid)` | Yes | Org that will own the new key |
| `name` | `string` | Yes | Human-readable label shown in the dashboard and audit log |
| `scopes` | `string[]` | No | Defaults to `["read", "write"]`. Must be a non-empty array when present. |
| `wallet_id` | `string (uuid) \| null` | No | Bind the key to a single wallet. Omit or send `null` to create an org-wide tenant key. |

```bash theme={null}
curl -X POST https://api.axon402.com/v1/api-keys \
  -H "Authorization: Bearer $AXON_ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "organization_id": "2f1a...-...",
    "name": "ci-automation",
    "scopes": ["read", "write"],
    "wallet_id": null
  }'
```

## Response

### Success (`201 Created`)

```json theme={null}
{
  "id": "uuid",
  "plaintext": "axon_sk_...",
  "prefix": "axon_sk_xxxxxxxxxxxx",
  "organization_id": "uuid",
  "wallet_id": null,
  "name": "ci-automation",
  "scopes": ["read", "write"],
  "created_at": "2026-04-08T00:00:00.000Z",
  "connect_snippets": {
    "recommended": "...",
    "claude_code": "...",
    "json_config": "...",
    "agent_prompt": "...",
    "claude_skill": "..."
  },
  "mcp_url": "https://api.axon402.com/v1/mcp",
  "operator_mcp_url": "https://api.axon402.com/v1/operator/mcp"
}
```

> [!WARNING]
>
> The `plaintext` field is returned **exactly once** and is never logged. Store it immediately in a secret manager (or in `~/.axon/operator.json` for `@axon402/cli auth login`). Lost the key? Rotate: create a new one and deactivate the old one.

### Response Fields

| Field | Type | Description |
| :- | :- | :- |
| `id` | `string` | UUID of the new `api_keys` row |
| `plaintext` | `string` | The full `axon_sk_*` token. Returned once, never logged, not retrievable later. |
| `prefix` | `string` | Deterministic, low-entropy prefix of the key. Safe to display in UI listings so operators can identify which key is which. |
| `organization_id` | `string` | The owning org (echo of the request) |
| `wallet_id` | `string \| null` | The bound wallet if this is a wallet-scoped key; `null` for org-wide tenant keys |
| `name` | `string` | Echo of the request `name` |
| `scopes` | `string[]` | Echo of the request `scopes` (or the `["read","write"]` default) |
| `created_at` | `string` | ISO-8601 timestamp of insertion |
| `connect_snippets` | `object` | Copy-paste onboarding snippets for the minted runtime key |
| `mcp_url` | `string` | Canonical runtime MCP endpoint for the minted key |
| `operator_mcp_url` | `string \| null` | Canonical operator MCP endpoint. Present only for org-wide keys. |

## Errors

### `400 VALIDATION_ERROR`

Returned when:

* `organization_id` is missing or not a valid UUID
* `name` is missing or empty
* `scopes` is present but empty
* `wallet_id` is present but belongs to a different organization

### `403 REJECTED_POLICY`

The caller is authenticated but cannot mint the requested key. Common causes:

* wallet-bound key trying to mint an org-wide key
* wallet-bound key trying to mint a key for a different wallet
* caller lacks write authority on `organization_id`

### `404 NOT_FOUND`

`wallet_id` was supplied but does not exist (or the caller cannot see it).

## Hash function

The server stores `HMAC-SHA256(plaintext, AXON_API_KEY_HASH_PEPPER)` — never the plaintext itself. The pepper lives in Cloud Run's Secret Manager and is not accessible from the database. A DB leak alone is not enough to reverse or brute-force a stored key.

`verifyDatabaseApiKey` (the function that runs on every authenticated request) uses the **same** HMAC function, which is why all new keys must be issued through this server-side route — client-side hashing with a different function would produce unverifiable keys (the bug that motivated this route's creation, see CHANGELOG 2026-04-08).

## See also

* [`@axon402/cli`](https://docs.axon402.com/docs/cli/installation#step-2-the-operator-cli) — the primary consumer of org-wide tenant keys
* [GET /v1/me](https://docs.axon402.com/docs/api-reference/organizations/me.md) — introspect which org + scopes a key resolves to
* [Authentication](https://docs.axon402.com/docs/operations/authentication.md) — overall auth model for the Decision API


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.