internkim
API

API overview

A token belongs to a person, and every call runs as that person.

Every tool internkim uses inside a company is callable from outside it, over HTTP, at https://api.intern.kim/v1. Every company we host calls that one address; the token is what says which company, and which person inside it. A company hosting internkim itself calls its own host at /api/v1, which is the same API on the same code. A token belongs to a person and every call runs as that person, so a token can never do more than its owner can.

The web app answers the request. It resolves the caller, then answers what it knows: the tool catalog, the tokens a person holds, a file kept for a later call, and every tool that reads or writes the record itself. What is left needs the company's computer — the messenger, the workspace, the browser — so those calls are carried there and the answer comes back.

Getting a token

The first token is made signed in, from the personal access tokens panel in settings. It is shown once.

After that a token makes its own successors, so a script rotates one without a browser:

curl -X POST https://api.intern.kim/v1/token \
  --header "Authorization: Bearer $INTERNKIM_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{"name":"reporting script","permission":"write"}'
{ "name": "reporting script", "permission": "write", "expiresAt": "2027-01-01T09:00:00.000Z", "token": "ik_..." }

token is answered once and only its SHA-256 hash is kept, so a lost token is replaced rather than recovered. permission defaults to the rung the calling token holds, and a token may never mint one that reaches further than itself. name may be left out, and the token is called pat-1, pat-2, and so on by the first ordinal the member does not already hold.

A token expires 90 days after it is made, or after expiresInDays, a whole number from 1 to 365. Calling with an expired token answers 401, and making another by the same name renews it.

GET /v1/tokens lists the names, rungs, expiry and last use of the tokens the member holds, and is the only plural path: POST /v1/token makes one and DELETE /v1/token?name=<name> revokes one, because each acts on exactly one token.

Rotating a token

A token cannot revoke itself, and that is what makes rotation safe: the replacement has to answer before the token it replaces goes away.

  1. POST /v1/token with the old token, naming the new one.
  2. Call something with the new token and see it answer.
  3. DELETE /v1/token?name=<old> with the new token.

Naming the presented token in either call answers 409.

Authentication

Every other endpoint takes the token as a bearer header:

curl https://api.intern.kim/v1/tools \
  --header "Authorization: Bearer $INTERNKIM_TOKEN"

The owner is re-resolved on every request. When the owner stops being active member, every call with their token answers 403 from that moment on; nothing needs to be cleaned up first.

A signed-in web session works the same way: the bearer may be the session's own Supabase access token. The session is the person, so it carries full permission — the web app calls these endpoints with it, and a script that already holds a session can too. Issued keys exist for everything that should not hold a session: they name their own permission rung and survive sign-out.

Connecting an MCP client

https://api.intern.kim/v1/mcp serves the same catalog as a Streamable HTTP MCP server, and a client that implements MCP authorization needs nothing but that address:

claude mcp add --transport http internkim https://api.intern.kim/v1/mcp
claude mcp login internkim

A call without a token answers 401 with WWW-Authenticate: Bearer resource_metadata="https://api.intern.kim/.well-known/oauth-protected-resource/v1/mcp". That document names the plane's Supabase Auth as the authorization server. The client registers itself there, opens intern.kim/oauth/consent for the person to sign in and allow it, and keeps the token it is given. The token acts as that person, at the rung they chose on the consent screen: read, or read and write. An app nobody chose a rung for reads, and none administers the company. The app is listed under connected apps in settings, and disconnecting it refuses its next call.

Anyone can register a client under any name, so the consent screen also shows where the access goes. A client on your own computer returns to localhost or its own app address. When the address is a website instead, the screen warns that the site receives the access; allow it only if you started connecting and know that site.

Starting from nothing

The person that window opens for does not need an account yet. The sign-in card offers to claim an invited address or to start a company, and either one carries the pending authorization, so they come back to the consent screen and allow the client there.

POST /api/company, the app's own endpoint that founds a company for the signed-in account, accepts an OAuth access token, so a client can take the person that far itself. One account founds one company.

Connecting the company computer is not open to a client. POST /api/company/host-setup, which answers the connection file, and the routes that claim a box, invite or remove members, and choose a connected app's rung refuse both an app's token and an ik_ token: only the person's own session reaches them. The tools for the same acts are held to the token's rung instead. person_invite takes a write token. person_withdraw, person_password_reset and person_message_delete are destructive, so they take an ik_ token with the delete rung, which no app's token reaches. What the connection file is for is in the Quickstart, and the install runs on that computer, so nothing here performs it.

Until the account belongs to a company, every tool call answers 403 naming that as what is missing, and the same client succeeds once it does.

The internkim plugin declares the same server in its mcp.json, so installing the plugin connects it the same way. A client without MCP authorization sends an ik_ token as the bearer header.

Permissions

A token holds one permission, and there are three of them. Each reaches everything below it, and what a tool needs comes from its sideEffectClass, never from its name.

PermissionOpens
readtools whose sideEffectClass is read
writethose, plus every writing class: workspace writes, external edits and sends, publishing, connections, browser actions, and the agent endpoints
deleteeverything, including deletes

The writing set is the list in web/src/lib/server/public-api/catalog.ts, and a descriptor carrying a sideEffectClass that is not in it needs delete — in the app that lists the tools and again on the machine that runs one. An unrecognized class fails closed at both ends.

Errors

Success bodies are JSON. Error bodies are plain text, with two exceptions the app answers as JSON: the catalog's own {"error":"not found"}, and a hint refusal, which carries errorCode, failureStage, retryable, safeRetry, hint and candidates so the next call can name one.

Two machines answer, and they do not share their wording. The app answers everything it resolves itself:

StatusBodyWhen
400decode error, this call carried a body that is not a json object, input is the object the tool readsthe body is not valid JSON, or input is not an object
400<field>: <what the schema says>, such as input.title: Invalid input: expected string, received undefinedinput does not satisfy the tool's published inputSchema
401sign in firstno Authorization: Bearer header, or the token names no account
401that key belongs to nobodythe ik_ token is unknown or revoked
403refusedthe account signed in is a member of no company
403token owner is not active memberthe ik_ token's owner left or was deactivated
403this token may only <permission>, and <tool> <permission>sthe token's permission is below what the tool requires
403write permission requiredan agent endpoint with a read-only token
404{"error":"not found"}GET /v1/tools/<name> for a tool this token cannot see
409a token cannot revoke itself...DELETE /v1/token naming the token that authenticated the call
409the hint refusal, with errorCode, hint and candidatesa hint that matched more than one thing, came close to some, or matched nothing
500the control plane is not configuredthe app is missing its Supabase settings
502the gateway answered <status>, and no status a caller can usethe company answered with nothing a caller can use
503the company gateway is not configuredan invocation where nothing knows how to reach the company

A call carried to the company is answered by admind, in its own words:

StatusBodyWhen
401bearer token required, invalid bearer tokenthe company machine did not recognize the call
403token owner is not active memberthe owner left or was deactivated
403asserted requester is not active memberthe named requester is not a member
403tool is not available for this tokenan unknown tool, or one above the token's permission
502whatever failed to resolvethe descriptor or the actor could not be read

Host diagnostics

Company administrators can read the work-history page's existing views through GET /v1/diagnostics/{view} or the host_diagnostics_get MCP tool. Both reuse the page's readers and return its recorded evidence. The served reference lists the views and query fields from the tool contract.

Keeping a file

POST /v1/files takes the bytes themselves, up to 25 MB, with the file's own content type and an optional ?filename=. The file is kept in the company's store and materialised into the workspace, and the answer names the path a later call can reach it by. A reading token is refused here, because keeping bytes anywhere is a write.

Revoking a token

DELETE /v1/token?name=<name> forgets the token by that name, and the next call carrying it is refused. A token cannot revoke itself; revoke it with whichever token replaced it.

The served reference

docs.intern.kim/api serves an interactive reference, in Korean or English, rendered from an OpenAPI document at docs.intern.kim/openapi/<language>.json. /api-docs and /openapi/<language>.json on any company host redirect there. The base tools in that document are read from the same catalog the agent runs on.

Where it is implemented

The API is part of the web app, so a company that hosts the app hosts the API. api.<zone>/v1 and <company host>/api/v1 are the same routes.

ConcernWhere
Routing for everything under /api/v1web/src/routes/api/v1/; /v1/* is rewritten onto it in web/src/hooks.ts
Who is calling, from a session token or an ik_ keycallingMember in web/src/lib/server/member-request.ts
Token issue, ik_ format, SHA-256 hashing, storeissuePersonalAccessToken in web/src/lib/server/control-plane.ts; the rules in web/src/lib/server/public-api/tokens.ts
Permission rungs and the tools each reachesweb/src/lib/server/public-api/catalog.ts, over web/src/lib/public-api-permission.ts
Carrying a tool call to the company machineweb/src/lib/server/public-api/company-call.ts, through workers/connection-gateway to the company's relay
What the company machine does with ithandlePublicAPI in internal/admind/public_tool_gateway.go, then invokeCapabilityTool over the capabilityd socket
Agent endpointsinternal/admind/agent_drive.go, forwarding to Blueclaw through blueclawJSONRequest
Wire typespkg/capabilityprotocol/protocol.go
Tool catalogpkg/capabilityprotocol/generated/capability-tools.json
The contract, stated as testsweb/tests/integration/public-api-route.test.ts, internal/admind/public_tool_gateway_test.go
MCP authorization: the resource metadata and the 401 challenge, then consentweb/src/lib/server/public-api/protected-resource.ts, web/src/routes/oauth/consent/, [auth.oauth_server] in supabase/config.toml
OpenAPI document and the /api-docs redirectdocs/web/app/lib/openapi.ts, web/src/lib/server/api-reference-redirect.ts

On this page