IncarnaDocsGitHubConsole

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

curl -X POST $BASE/keys \
  -H 'Content-Type: application/json' \
  -d '{"name":"production-backend"}'
{
  "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

curl $BASE/keys          # console session
[
  {
    "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.

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.