# Authentication

Every `/agents*` route is scoped to an organization. There is one auth primitive —
an API key — and one place keys are issued from.

```
Authorization: Bearer ik_live_2718ddec_a1b2c3d4e5f6...
```

## Key format

```
ik_live_2718ddec_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
└─────prefix────┘└──────────── secret ───────────┘
```

We store the **prefix** and `sha256(full key)`. The secret half is returned exactly
once, at mint time, and never again — not in listings, not in support, not from the
database. A lost key is replaced, not recovered.

The prefix is safe to log and display. It is how a key appears in `GET /keys` and
what you pass to revoke one.

## Minting a key

```sh
curl -X POST $BASE/keys \
  -H 'Content-Type: application/json' \
  -d '{"name":"production-backend"}'
```

```json
{
  "key": "ik_live_2718ddec_a1b2c3d4e5f6...",
  "prefix": "ik_live_2718ddec",
  "name": "production-backend"
}
```

**This route requires a signed-in console session.** An API key cannot mint another
API key, and that restriction is intentional rather than an oversight:

> A key that can mint keys outlives its own revocation. Leak it once, the holder
> mints a replacement, and revoking the first accomplishes nothing. Issuance stays on
> the surface where a human authenticated — a credential that can actually be taken
> back.

Calling `POST /keys` with an API key returns `403 forbidden`.

## Listing and revoking

```sh
curl $BASE/keys          # console session
```

```json
[
  {
    "prefix": "ik_live_2718ddec",
    "name": "production-backend",
    "created_at": "2026-08-02T13:41:02.118Z",
    "last_used_at": "2026-08-02T14:02:55.907Z",
    "revoked_at": null
  }
]
```

`last_used_at` is updated on every authenticated request, which makes it the fastest
way to find keys nobody is using any more.

```sh
curl -X DELETE $BASE/keys/ik_live_2718ddec
```

Revocation is immediate — the next request with that key gets `401`. Revoking is
scoped to the calling organization: a prefix belonging to another tenant returns
`404`, not a cross-tenant revoke. Prefixes are visible to whoever holds the key, so
knowing one must not be enough to disable it.

Revoked keys stay in the listing with `revoked_at` set. They are not deleted, so the
audit trail survives.

## The console path

The web console does not hold an API key. It authenticates the browser with Clerk,
and its server tier forwards the verified identity to the API:

```
Authorization: Bearer <INCARNA_SERVICE_SECRET>
X-Incarna-Clerk-Id: user_...
X-Incarna-Email:    someone@example.com
X-Incarna-Name:     Someone
```

The bearer here is a shared secret held only by the backend-for-frontend, never by
the browser. This path is what `POST /keys` accepts and what MCP deliberately does
**not** — MCP callers are programmatic and should carry a credential that can be
revoked on its own.

## Rate limiting

**120 requests per minute, per organization**, on a rolling 60-second window.
Exceeding it:

```
HTTP/1.1 429
{"error":{"code":"rate_limited","message":"rate limit exceeded (120/min)"}}
```

The limit is per org, not per key, so minting more keys does not raise it.

## Handling credentials

Incarna stores customer platform credentials encrypted at rest and never logs them
in plaintext. Two things are expected of you in return:

- **Never put an Incarna key in client-side code.** It is an org-scoped credential;
  anything holding it can operate every agent in the org.
- **Rotate anything that has been pasted somewhere it shouldn't be** — a chat, a
  ticket, a screenshot. Mint a replacement, deploy it, then revoke the old prefix.
  In that order; revoking first causes an outage.
