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

# POST/GET/DELETE /v1/keys

> Create, list (masked), revoke — V1 access rules

<Note>
  `/v1/keys` is **excluded** from the standard `/v1/*` authentication
  (`ApiKeyAuthMiddleware`): it has its own logic, described here. See also
  [Security](/en/security) for the full model.
</Note>

## Access rules (V1, deliberately simple)

<Tabs>
  <Tab title="HAKI_ADMIN_KEY set — admin mode">
    All key management requires
    `Authorization: Bearer <HAKI_ADMIN_KEY>`. The admin freely chooses
    `org_id`/`project_id` at creation and sees every key in the listing.
  </Tab>

  <Tab title="HAKI_ADMIN_KEY unset — bootstrap">
    The **first** key creation is free (the `api_keys` table is empty).
    After that, a valid key only manages the keys of **its own**
    project: creation is bound to its scope, listing and revocation are
    scoped to it. A revoke targeting another project's key returns the
    same `404 key_not_found` as an unknown id.
  </Tab>
</Tabs>

## Creating a key

```http theme={null}
POST /v1/keys
```

<ParamField body="org_id" type="string" required>1 to 128 characters.</ParamField>
<ParamField body="project_id" type="string" required>1 to 128 characters.</ParamField>
<ParamField body="label" type="string">Max 128 characters.</ParamField>

### Response — `201 Created`

<ResponseField name="key" type="string" required>
  The clear key (`hk_...`) — **returned only once, here**. Only its
  sha256 hash is stored afterwards; it can never be retrieved again.
</ResponseField>

<ResponseField name="id" type="uuid" required />

<ResponseField name="prefix" type="string" required>The first 8 characters, for masked listings.</ResponseField>

<ResponseField name="org_id" type="string" required />

<ResponseField name="project_id" type="string" required />

<ResponseField name="label" type="string | null" />

<ResponseField name="created_at" type="datetime" required />

<RequestExample>
  ```bash curl theme={null}
  curl -X POST http://localhost:8100/v1/keys \
    -H "Content-Type: application/json" \
    -d '{"org_id": "org_acme", "project_id": "prj_support", "label": "dev"}'
  ```

  ```bash CLI theme={null}
  uv run haki keys create --project-id prj_support --org-id org_acme --label dev --save
  ```
</RequestExample>

***

## Listing keys (masked)

```http theme={null}
GET /v1/keys
```

Without `HAKI_ADMIN_KEY`, the listing is scoped to the calling key's
project.

<ResponseField name="keys" type="KeyOut[]" required>
  Never the key nor its hash — only `prefix`.

  <Expandable title="KeyOut">
    <ResponseField name="id" type="uuid" required />

    <ResponseField name="prefix" type="string" required />

    <ResponseField name="org_id" type="string" required />

    <ResponseField name="project_id" type="string" required />

    <ResponseField name="label" type="string | null" />

    <ResponseField name="created_at" type="datetime" required />

    <ResponseField name="revoked_at" type="datetime | null" />
  </Expandable>
</ResponseField>

***

## Revoking a key

```http theme={null}
DELETE /v1/keys/{key_id}
```

Sets `revoked_at` — **immediate** effect: the key returns
`401 unauthorized` on the very next call.

<ResponseField name="id" type="uuid" required />

<ResponseField name="status" type="string" default="revoked" />

## Possible errors

`unauthorized` (401), `forbidden_scope` (403 — a key tries to create a
key for an `org_id`/`project_id` other than its own), `key_not_found`
(404).

***

## Organization provisioning (console-only)

```http theme={null}
POST /v1/orgs/provision
```

<Warning>
  **Internal** endpoint, called only by the console's Next.js backend
  with an already-verified Clerk identity — never by a browser, never by
  a customer's `hk_` key. Authenticated by a single shared secret
  (`HAKI_CONSOLE_SERVICE_KEY`), not the standard key model. Not covered
  by the SDK.
</Warning>

Takes `{owner_ref, name}`; generates a **server-side** `org_id` (never
supplied by the caller, unlike the `/v1/keys` bootstrap), creates a
`prj_<org_id>_default` project and a key. A repeat call for an already
known `owner_ref` does **not** recreate the organization: it mints a new
key on the same project instead (`org_created: false`).
