---
title: "Create"
method: POST
path: "/api-keys"
tags: ["api-keys"]
---

# Create

`POST /api-keys`

## Request

- Base URL: `https://api.instruxi.dev/api/v1/enforcer`
- URL: `https://api.instruxi.dev/api/v1/enforcer/api-keys`
- Auth: one of:
  - API key in header `Authorization`
  - API key in header `X-API-Key`

Mints a new API key for the CALLER's own account and returns the plaintext exactly once in this response — it is never retrievable again. name is required and cannot be blank: an unnamed key is a working credential nobody can identify to revoke later, so an empty or whitespace-only name is rejected with 400 missing_fields rather than silently minting an anonymous key. scope is optional and space-delimited, in the same vocabulary OAuth grants (enforcer:read | enforcer:write | enforcer:admin | policy:self | enforcer:<tag>.<effect>); the key stores it and the response echoes it. A key never exceeds its owner's role and permissions, and a scope narrows it further. A caller whose own credential carries a scope may only mint at or below it (the coarse ladder read < write < admin; a fine scope is covered by its own tag at an equal-or-wider effect or by a covering coarse scope) and is refused with 403 insufficient_scope otherwise; such a caller that omits scope gets a key carrying its own scope, never an unscoped one. An unscoped caller (a login session, an unscoped API key) may mint any scope or none. An unknown scope is 400 invalid_scope. expires_at is optional, RFC 3339, and must be in the future (400 invalid_expires_at otherwise); once it passes the key is refused with 401 on both the HTTP X-API-Key path and the gRPC transport, exactly like a revoked key. Omitted, the key never expires. The response records created_by, the account that minted the key. A caller presenting an agent (ag_, or an agent client_credentials token) or portal-link credential may never mint a key: 403 agent_cannot_mint (AGENTS_CONTRACT §5.3).

## Request body

- InternalDeliveryHttpHandlerCreateKeyRequest
  - `expires_at` string, date-time: ExpiresAt is when the key stops authenticating, RFC 3339. Optional; omitted mints a key that never expires. Must be in the future.
  - `name` string
  - `scope` string: Scope narrows the key, space-delimited, in the OAuth vocabulary: enforcer:read | enforcer:write | enforcer:admin | policy:self | enforcer:<tag>.<effect>. Omitted means unnarrowed for an unscoped caller, and the caller's own scope for a scoped one.

## Response `201`

Created

- InternalDeliveryHttpHandlerApiKeyCreatedResponse
  - `data` EnforcerV3InternalUsecaseApikeysCreated
    - `api_key` EnforcerV3InternalDomainApikeyAPIKey
      - `account_id` string
      - `active` boolean
      - `created_at` string
      - `created_by` string: CreatedBy is the account that minted the key: provenance, never authority. Equal to AccountID for a self-minted key; nil for keys minted before 000111.
      - `expires_at` string: ExpiresAt is when the key stops authenticating (000111). nil — every key minted before the column existed — never expires. Both transports refuse an expired key through Expired, so the rule cannot differ between them.
      - `id` string
      - `key_prefix` string
      - `last_used_at` string
      - `name` string
      - `scope` string: Scope narrows what the key may do below its owner's grant. nil is the only value existing keys carry and means unnarrowed — the key acts with the account's full permissions, exactly as before the column existed. Omitted from JSON when nil, so a response for an unscoped key is byte-identical to what it was.
      - `tenant_id` string
      - `updated_at` string
    - `key` string: full plaintext — shown once, never again
  - `success` boolean

## Other responses

- `400`: Bad Request
- `401`: Unauthorized
- `403`: insufficient_scope | agent_cannot_mint
- `500`: Internal Server Error

---

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