---
title: "Create an API key"
method: POST
path: "/keys"
tags: ["Credentials"]
---

# Create an API key

`POST /keys`

## Request

- Base URL: `https://api.primitive.dev/v1`
- URL: `https://api.primitive.dev/v1/keys`
- Auth: HTTP bearer

Create an API key, super admin or narrowed to a preset or a custom permission set, optionally with an expiry and conditions. The secret is returned once. A key never holds more than the person creating it; the credential policy decides who may create which keys. grant is required: a request without it is refused (400 validation_error, details.reason grant_required), never treated as super admin. A custom key must say which mail it reaches: the addresses and domains it lists (grant.addresses, grant.domains), or every mailbox in the organization with grant.mail_scope "org", which cannot be combined with them; one that says neither is refused (400 validation_error, details.reason mail_scope_required). Super admin, Send only and Read only reach every mailbox by definition. members:read is only for an org-wide custom key: naming it with grant.addresses or grant.domains is refused (400). Owners and admins are emailed about every super admin key. Send an Idempotency-Key to retry safely: a second create by the same person under the same key makes no second key and is refused (409 idempotency_key_reused) with the first key's id in error.details.key_id, so a caller whose first answer was lost can revoke that key. The onboarding field is for the dashboard only. The Primitive CLI's token is refused, even with a step-up: keys are created in the dashboard or by an app the person connected. While the organization's keys API is not enabled, this answers 404 feature_disabled, except that a dashboard session may still create a super admin key (the dashboard's own create).

Person only: a dashboard session, or the person's own OAuth token with a step-up token for purpose `keys.create` in the Primitive-Step-Up header. Without one an OAuth token is answered 403 step_up_required, whose details carry the dashboard URL that issues it (or 403 grant_forbidden when the person's role could not do it anyway). The person's role must hold keys:manage. API keys and agent connection keys are refused. An agent acting for the person is refused unless it carries a step-up token the person issued in the dashboard.

## Headers

- `Primitive-Step-Up` string
- `Idempotency-Key` string
- `Primitive-Not-After` string, date-time

## Request body

- CredentialsCreateKeyInput
  - `name` string
  - `grant` union, required
    - 'super_admin' | 'send_only' | 'read_only' | 'own_addresses' | 'own_addresses_read_only' | 'custom'
    - object
      - `preset` 'super_admin' | 'send_only' | 'read_only' | 'own_addresses' | 'own_addresses_read_only' | 'custom', required
      - `matrix` object
      - `permissions` string[]
      - `addresses` string[]
      - `domains` string[]
      - `rules` string[]
      - `depth` 'metadata' | 'content' | 'raw'
      - `resources` object
      - `mail_scope` 'org'
  - `expires_at` string, date-time
  - `expires_in_days` integer
  - `conditions` object
    - `ip_allowlist` string[], nullable
    - `rate_limit_per_minute` integer, nullable
  - `onboarding` boolean

## Response `201`

Created

- object
  - `success` true, required
  - `data` CredentialsCreateKeyResult, required
    - `id` string, required
    - `name` string, nullable, required
    - `key` string, required
    - `key_prefix` string, required
    - `grant` object, required
      - `template` string, nullable, required
      - `credential_grant_id` string, nullable, required
      - `permissions` string[], nullable, required
      - `mail_scope` unknown, required
      - `resources` unknown, required
    - `expires_at` string, date-time, nullable, required
    - `conditions` unknown, required
    - `warnings` object[], required
      - `permission` string, required
      - `adds` string[], required
      - `why` string, required
      - `description` string, required
    - `address_warnings` object[]
      - `address` string, required
      - `reason` string, required
      - `description` string, required
    - `replaced_key_ids` string[]

## Other responses

- `400`: Invalid request parameters
- `401`: Invalid or missing API key
- `403`: Authenticated caller lacks permission for the operation
- `404`: Resource not found
- `409`: The request conflicts with the current state of the resource
- `429`: Rate limit exceeded. The `ratelimit-*` headers describe the limiter that rejected this request, with its own limit and window, not the API budget that successful responses report. A limiter that reports only a wait sends `Retry-After` alone.

---

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