# API keys & authentication

Send the credential in the Authorization header: `Authorization: Bearer hk_live_…`. The same header works for REST, the SDK, the CLI and the MCP connector.

Owners and admins create keys in Dashboard → Settings → API access. A key is shown once and stored only as a hash. It works until it is revoked, unless you give it an expiry date, and an owner or admin can pause it without revoking it (below). Revoking takes effect immediately.

## Kinds of key

| Key | Acts as | Can |
| --- | --- | --- |
| Full, whole workspace | developer | Create, change and delete resources in every project, deploy, send mail, use AI, buy domains from the Balance. It can also delete projects. |
| Full, one project | developer | The same, but only on that project's endpoints, plus sending mail as that project and calling AI for it. Workspace-wide endpoints refuse it. |
| Read-only, whole workspace | viewer | GET requests, plus reads sent as POST (vector query, memory search, storage download links). No environment variables, no SQL, no uploads. |
| Read-only, one project | viewer | Reads on that project only. |

> No API key can manage billing, members or other keys, or move money out: withdrawals, refunds and payout setup need the workspace owner signed in to the dashboard.

## Which key to give what

| Use | Key |
| --- | --- |
| Your laptop, CI, setup scripts | Full, whole workspace |
| A deployed app that uses its database, storage, payments, vectors, memory or auth pool | Full, one project |
| A deployed app that also sends mail or calls the AI Gateway | Full, one project: it sends mail and calls AI for its own project, paid for by the workspace |
| Dashboards, monitoring, read-only AI assistants | Read-only |

## Where a project key works

A project key passes every route that names its project: `/api/projects/<id>/…`, and item routes whose resource belongs to it — `/api/databases/:id`, `/api/storage/:id`, `/api/functions/:id`, `/api/vectors/:id`, `/api/memory/:id`, `/api/agents/:id`, `/api/payments/:accountId`, `/api/auth-pools/:id`, `/api/deployments/:id`.

It also sends mail (`POST /api/mail/send`) and calls the AI Gateway (`/api/ai/chat`, `/api/ai/playground`, `/api/ai/v1/chat/completions` and `/api/ai/embeddings`) for its own project. Leave `projectId` out (for AI, also the X-Harakumo-Project header) and the call acts for the key's project; naming another project is refused. The email quota and the AI spend are the workspace's, as with any key, and a read-only key can neither send mail nor spend on AI.

It is refused, with 403 "This API key only works on project N", on workspace-wide routes: listing projects, the rest of `/api/mail/*` (messages, settings, suppressions and sending domains), `/api/ai/requests`, `/api/domains/*`, `/api/repos/*`, `/api/usage` and billing.

Its mail goes out only as its own project. It sends from an address on the project's own sending domain (the domain of the project's saved From address) or from the shared no-reply; another domain of the workspace answers 403 `from_not_project_domain`. It never sends under another project's name or sender name, or a close variant of one (a name that contains one of five letters or more, or is one letter off one of six or more): 403 `sender_name_other_project`. A variant of a name its own project's name already contains is its own ("Fruitly API" may send as "Fruitly Receipts"), but another project's exact name never is. The same rules apply when it changes its project's `mailFrom` or `mailSenderName` with `PATCH /api/projects/:id`. To move a project to a new sending domain, a person sets its From address on the Mail page.

## Create a key with the API

**curl (an owner or admin session or key)**

```bash
curl -X POST https://harakumo.com/api/orgs/<workspaceId>/api-keys \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "name": "notes-app production", "access": "full", "projectId": 12 }'
# 201 → { "key": { "id", "prefix", "access", "project_id" }, "secret": "hk_live_…" }   (secret shown once)
```

> Creating keys needs the admin role, so an API key (developer or viewer) cannot mint more keys. Use the dashboard, or an admin's connector.

## Expiry and pause

- Give a key an end date: `PUT /api/orgs/:id/api-keys/:keyId/expiry { days }` (1 to 730, counted from now) or `{ expiresAt }` (a date within 730 days); `null`, or `DELETE` on the same path, means it never expires. Past the date every request with it is refused with 401 `key_expired`. An expired key given a new date works again.
- Pause a key that may have leaked but that an app you cannot update yet still holds: `POST /api/orgs/:id/api-keys/:keyId/pause`. Every request with it is refused with 403 `key_paused` until `DELETE` on the same path resumes it. Nothing else about the key changes.
- Both need an owner or admin, answer the key with its state (active, expired, paused or revoked), and are written in the workspace's audit log. The same controls are in Settings → API access and in an admin's AI connector (set_api_key_expiry, pause_api_key).
- A key made by a person whose account Harakumo has suspended is refused too (403 `key_creator_suspended`) until the account is restored. Create a new key for the app.

## Other credentials

| Credential | Where it comes from | Acts as |
| --- | --- | --- |
| Browser session | Signing in to the dashboard; an httpOnly cookie, 30 days | You, with your role in the chosen workspace |
| Connector token (hk_mcp_…) | Connecting Claude, ChatGPT or another MCP client (OAuth) | You, read-only or read-and-manage, in one workspace or all of them |
| Auth-pool client secret (as_…) | Enabling an auth pool | Only that pool's sign-in actions — never anything else |
| Auth-pool client id (ap_…) | Enabling an auth pool | Publishable: browser sign-in for that pool, when turned on |

## Choosing a workspace

A key belongs to one workspace. A session or an all-workspaces connector acts in the workspace named by `?org=<id>` or the `x-harakumo-org` header, else the one last chosen in the dashboard.

## When a key is refused

| Status | Message | What to do |
| --- | --- | --- |
| 401 | Unauthorized | The header is missing, or the key was revoked or mistyped. |
| 401 `key_expired` | This API key expired on … | Give the key a new date, or create a new key. |
| 403 `key_paused` | This API key is paused | An owner or admin resumes it in Settings → API access. |
| 403 `key_creator_suspended` | This API key was created by an account that is suspended | Create a new key; nothing has been deleted. |
| 403 | This API key is read-only | Use a full key for writes. |
| 403 | This API key only works on project N | Use a key for that project, or a workspace key for workspace-wide routes. |
| 403 | Your role (developer) does not allow this action | The action needs an admin or owner (members, keys, billing). |
| 404 | Not found | The id does not exist or belongs to another workspace — the two look the same on purpose. |

## Reference

**SDK**

```js
import Harakumo from '@harakumo/sdk';
const hk = new Harakumo({ apiKey: process.env.HARAKUMO_API_KEY });   // baseUrl defaults to https://harakumo.com
// Errors throw HarakumoError with .status and .body (the JSON the API sent).
```

**CLI**

```bash
harakumo login --key hk_live_…     # or set HARAKUMO_API_KEY; --key and --url work on every command
harakumo whoami
```

**REST**

```http
Authorization: Bearer hk_live_…
GET    /api/orgs/:id/api-keys                 # list (admin+); secrets are never returned
POST   /api/orgs/:id/api-keys                 { "name", "access": "full" | "read", "projectId"? }
PUT    /api/orgs/:id/api-keys/:keyId/expiry   { "days": 90 } | { "expiresAt": "2027-01-31" } | { "days": null }
DELETE /api/orgs/:id/api-keys/:keyId/expiry   # never expires
POST   /api/orgs/:id/api-keys/:keyId/pause    # 403 key_paused until DELETE resumes it
DELETE /api/orgs/:id/api-keys/:keyId          # revoke
```
