---
title: "Mint an API key"
method: POST
path: "/v2/workspaces/{id}/keys"
tags: ["Keys"]
---

# Mint an API key

`POST /v2/workspaces/{id}/keys`

## Request

- Base URL: `https://api.infolang.ai`
- URL: `https://api.infolang.ai/v2/workspaces/{id}/keys`
- Auth: one of:
  - HTTP bearer
  - HTTP bearer
  - HTTP bearer

The key `secret` is returned once, in this response only — it cannot be retrieved again.

ATTENUATION: the optional `scopes`, `scoped_workspace_ids`, `scoped_namespaces` and `expires_at` must each be within the MINTING credential's own effective authority (scopes ⊆, workspaces ⊆, namespaces ⊆, expiry ≤), and within the minter's role permissions. A credential can never mint a stronger credential, so a machine key that can mint keys is safe: it can only ever mint weaker ones. Wildcards (`*`, `memories:*`, `*:read`) are accepted and stored FROZEN — the literal plus its expansion as of today — so a stored `*` is never widened by a resource added later; re-mint to pick new resources up.

Omitting every scoping field reproduces the pre-scoping behavior: on the session lane no scope record is written and the key resolves to `LEGACY_DEFAULT`; on the machine lanes the new key INHERITS the minter's own authority (never `LEGACY_DEFAULT`, which could be broader).

`scoped_namespaces` restricts the key to a set of namespaces (`null` or omitted inherits the minter's, which for an unrestricted minter is unrestricted; an empty array is REJECTED rather than read as unrestricted). An unrestricted minter may grant any set; a restricted one may only narrow. A namespace-restricted key is **/v2-only** — the `/v1/*` and `/mcp` passthrough denies it with 403, because the gateway does not parse those bodies and Kairos has no per-credential namespace scope, so the restriction would be bypassable by changing the URL prefix.

Requires the `keys:write` scope. Accepts all three lanes: a session JWT (bounded by the caller's role on this workspace) or an `il_*` machine credential whose own scope set, workspace set and validity window cover it — both ceilings apply and neither can raise the other, so a credential carrying `keys:write` is still refused where the role does not permit it.

A credential with no scope record resolves to `LEGACY_DEFAULT`, which does NOT include `keys:write` — mint a key with it explicitly (`POST /v2/workspaces/{id}/keys`).

## Path parameters

- `id` string, required

## Request body

- object
  - `env` 'live' | 'test', required
  - `label` string
  - `scopes` string[]
  - `scoped_workspace_ids` string[]
  - `scoped_namespaces` string[], nullable
  - `expires_at` string, date-time, nullable
  - `not_before` string, date-time, nullable

## Response `201`

Minted key, including the one-time secret

- object
  - `key_id` string, required
  - `env` 'live' | 'test', required
  - `prefix` string, required
  - `label` string, nullable
  - `created` string, required
  - `last_used` string, nullable
  - `secret` string
  - `scopes` string[]
  - `scoped_workspace_ids` string[]
  - `scoped_namespaces` string[]
  - `expires_at` string, nullable
  - `not_before` string, nullable
  - `principal` string
  - `principal_kind` 'user' | 'service'

## Other responses

- `401`: Missing/invalid credential, or `credential_expired` — the credential is outside its `[not_before, expires_at)` window and must be replaced
- `403`: `insufficient_scope` — the credential's own scope set, or the caller's role on this workspace, does not cover the scope this route requires (the missing scope is named in the message). Or `forbidden` — the credential is not scoped to this workspace, or holds no grant on it
- `422`: Invalid body, or an unknown scope not in the vocabulary

---

[API](https://skmtc.dev/infolang/apis/infolang-api.md) · [All operations](https://skmtc.dev/infolang/apis/infolang-api/llms.txt) · [OpenAPI document](https://skmtc.dev/infolang/apis/infolang-api/revisions/d04bf6de72c8?raw)
