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

# Create an API key

`POST /v1/keys`

## Request

- Base URL: `https://api.hyperresearch.ai`
- URL: `https://api.hyperresearch.ai/v1/keys`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `hr_session`

The secret is returned exactly once. The key is always minted into the calling key’s workspace. Pass `scopes` to narrow what the key may do; omit it for the default set (`runs:write`, `runs:read`, `vault:read`, `vault:write`, `verify`). Add `mcp` explicitly for a key a headless agent will present to the MCP server; it is never in the default set and `admin` does not imply it. Add `admin:read` — alone, on a key named `operator-diagnostics` — for the operator diagnostic read (`GET /v1/admin/runs*`); it grants no write and no health, billing or backfill endpoint, and `admin` implies it. A key form should show the scopes before minting: production minted write access to runs and the vault from a form that named none of it (E2E3-14).

## Request body

- CreateKeyBody
  - `name` string
  - `prefix` 'hr_live_' | 'hr_test_'
  - `scopes` string[]: What this key may do. Omit it for the default set — `runs:write`, `runs:read`, `vault:read`, `vault:write`, `verify` — which is what every key minted before this field existed carries. Valid values: runs:write, runs:read, vault:read, vault:write, verify, mcp, admin, admin:read. An unknown scope is a 400 on `scopes`, never a silently dropped field, and an empty array is refused: a key that can do nothing is a mistake. `admin` is owner-equivalent — it implies every other scope except `mcp` — so grant it deliberately. `mcp` lets this key open a session with the MCP server at `https://mcp.hyperresearch.ai/mcp` (`Authorization: Bearer <key>`), for a headless agent that has no browser for the OAuth flow. It is a front-door marker only: every tool still needs its own data scope, so pair it with `vault:read` / `runs:read` as needed. It is never granted by default and never implied by `admin`. `admin:read` is the operator diagnostic read (`GET /v1/admin/runs*`, `ops/diagnosing-a-run.md`): the one scope here that is not bounded by the key’s own workspace, and the one that grants no write anywhere and no health, billing or backfill endpoint. Grant it alone, on a key named `operator-diagnostics`. It is never in the default set; `admin` implies it, so granting both is redundant rather than wrong.
  - `expires_at` string, date-time
  - `ip_allowlist` string[]

## Response `201`

Created

- CreatedApiKey
  - `id` string, required
  - `name` string, nullable, required
  - `prefix` 'hr_live_' | 'hr_test_' | 'hr_chat_', required
  - `masked` string, required
  - `scopes` string[], required
  - `last_used_at` string, nullable, required
  - `expires_at` string, nullable, required
  - `revoked_at` string, nullable, required
  - `created_at` string, required
  - `ip_allowlist` string[], required
  - `rotate_by` string, nullable, required
  - `rotation_due` boolean, required
  - `secret` string, required

## Other responses

- `400`: Invalid request
- `401`: Authentication failed
- `403`: Insufficient scope or plan
- `404`: Not found
- `429`: Rate limited or over quota
- `500`: Internal error

---

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