DOCS
API
One API. Every action Korpus has is an op with a name and arguments: a program calls it over HTTP or with the SDK, an AI app calls the same op as an MCP tool. This reference is for developers; to use Korpus with Claude or ChatGPT, the manual is enough, and to connect an app, see Connect.
Overview
| Ops | One per action: list_folder, read_concept, create_concept, update_concept and the rest. Each has a name, a JSON input, an answer (data, and the text a model would read) and the doors it is served at. |
|---|---|
| Doors | API: POST https://api.fra.korpus.cloud/v1/<op>, plain JSON over HTTPS, for the web app, the SDK, the CLI, scripts and CI. MCP: https://mcp.fra.korpus.cloud/ and an address per access, for AI apps. Most ops are at both; a few are only on the API and manual is only on MCP. |
| Auth | A bearer token on every request, from a sign-in, a trusted workload or a key. A credential is bound to one door: at the other, 403 wrong_audience (Connect). |
| Who | me (your full rights) or a delegation (a job and path rules). An op is offered only to those it is for: me alone for managing ops, a reviewer's me for the review ops, which this reference leaves out. |
| SDK | @korpus-cloud/sdk: a typed client for the API, and the korpus command that signs a machine in. |
| Hosting | AWS eu-central-1 (Frankfurt). |
The data model in one breath: a bundle is a folder of concepts; a concept is a markdown document at a path such as auth/token-expiry, with type, tags, relations and a rev. Links in the web app are relations here. See the manual for the rest.
The API
https://api.fra.korpus.cloud/v1
Send the token as Authorization: Bearer …. No CORS: a token in a browser page is a token anyone can read.
GET /v1: what this credential may call.{api, delegation, rules, tools}, each op withname,title,description,read_only,destructiveand itsinputas JSON Schema.delegationisnullforme.apimoves only on a break.POST /v1/<op>with the arguments as a JSON object:200 {data, text}. A refusal is{"error": {"code", "message", …details}}with the HTTP status the failure has everywhere in Korpus (400 a bad input, 403 restricted or out of room, 404 not found, 409 a conflict, 412rev_mismatch, 429rate_limited, and 503maintenanceordatabase_startingwithRetry-After).
GET https://api.fra.korpus.cloud/v1
200 OK
{ "api": 1, "delegation": "nightly-sync", "rules": [ … ],
"tools": [ { "name": "list_folder", "title": "List folder", "description": "…",
"read_only": true, "destructive": false, "input": { …JSON Schema… } }, … ] }curl -fsS https://api.fra.korpus.cloud/v1/list_folder \
-H "Authorization: Bearer $KORPUS_KEY" \
-H 'Content-Type: application/json' \
-d '{"bundle": "01JBQ3M7X9VK2NDCTF8ZPYW4RH", "folder": "runbooks"}'200 OK
{ "data": { "entries": [ … ], "hidden": false },
"text": "…" }
412 Precondition Failed
{ "error": { "code": "rev_mismatch",
"message": "concept \"runbooks/deploy\" has changed since you read it (now rev 5); …",
"current_rev": 5, "updated_by": "…", "updated_at": "…" } }Refused before any op runs:
| Code | Meaning |
|---|---|
wrong_audience403 | A credential away from the door and address it was made for: another delegation's or another account's address, a delegation at /, or the API instead of MCP (or back). The message and details.url name where it works. |
account_blocked403 | The account is blocked. Every request is refused. |
unauthorized401 | No token, or one that is unknown, expired or revoked. |
unknown_op404 | No op of that name for this caller (or not at this door). GET /v1 lists them. |
invalid_arguments400 | The body is not JSON, or does not fit the op's input schema; the message names each field. Nothing ran. |
method_not_allowed405 | GET /v1 and POST /v1/<op> only. |
The data shapes are typed in the SDK (Tools in its type definitions); the text is the one an MCP client reads.
MCP
| Transport | Streamable HTTP, stateless: JSON responses, no session id, no server-sent events. |
|---|---|
| Address | https://mcp.fra.korpus.cloud/ is you, me, whoever signs in. https://mcp.fra.korpus.cloud/<username>/me is the same for one account, and https://mcp.fra.korpus.cloud/<username>/<delegation> is a delegation. Each request stands alone: JSON-RPC over POST, no stream to open and no session to end. |
| Auth | OAuth 2.1 with PKCE and dynamic client registration, or a key for a host that cannot sign in. Bearer token on every request. How to connect each app: Connect. |
| Server | korpus, titled Korpus · <delegation> for a delegation: which access the connection is. |
| Tools | The ops served at this door, plus manual. |
| Resources | Bundles and concepts as korpus:// URIs. |
| Prompts | whats_in_korpus, save_session, whats_new, ask_korpus, check_bundle, start_bundle. |
A client reads an address from its config. The .mcp.json shape Claude Code and similar clients read:
{
"mcpServers": {
"korpus": {
"type": "http",
"url": "https://mcp.fra.korpus.cloud/"
}
}
}Instructions
On initialize the server sends instructions a client passes to its model:
- Which Korpus deployment the client reached.
- A short overview: the data model, which tools only look, the change description every write carries, how to find things (list_folder first).
- The access's name and job (
meor a delegation) and, for a delegation, its rules.
Everything longer sits behind the manual tool, so a client that drops the instructions can still read them. Topics:
start | First moves in a bundle you do not know. |
concepts | Paths, fields, rev, create versus update, change descriptions, sources, body links and @-mentions, moves, many at once. |
assets | Files beside the concepts that explain them: upload by curl or as text, download, replace, file space. |
find | list_folder first; search, regex and body search, digests, date and stale filters; query for items by their fields. |
relations | Relation types and links between concepts. |
collections | Collections: folders of items sharing one shape; defining one (item type, fields, key, sections, refs and on_delete), what a write must fit, changing it. |
views | query and saved views: filters, sort, group, totals, across collections; table, list, board and tree. |
sharing | Sharing a bundle with a person (a share, accepted or left by them); renaming, deleting and exporting a bundle. |
catalog | Templates in the app: finding, reading and cloning one; publishing for review. |
delegations | me and delegations: rules, path patterns and rights; the two doors (MCP and the API); connections, sign-in, trust and keys. |
learning | A bundle that teaches the agents using it: read AGENTS and the last retro first, write a retro and apply one change last. |
limits | The ceilings every plan shares, and document and file space. |
errors | Every error code and what to do about it. |
support | When and how to file a ticket with send_feedback, so it can be reproduced without asking back. |
MCP: tools
One tool per action, each with a title a client shows and hints it sorts by. The reading tools carry readOnlyHint, so most clients let a person allow them for good; every other tool asks, and the ones that remove or end something also carry destructiveHint. Every tool is openWorldHint: false. A delegation gets no managing op at all.
- Change descriptions.
create_concept,update_concept,delete_concept,add_relationandremove_relationtake a requiredchange_description: one line, what changed and why. It is the log; nothing else records why a concept moved. - Index and log. Every folder has an
index(read_index: the titles and descriptions of its concepts) and alog(read_log: its changes, newest first). Both are generated from the content as OKF 0.2 §8 and §9 describe, never stored, so no concept may be namedindexorlog. - Revisions.
read_conceptreturns a concept'srev. Pass it back onupdate_conceptand the call fails withrev_mismatchif someone changed the concept meanwhile. Without it the update lands and says whose revision it changed.update_conceptchanges only the fields given;""clears a text field.
On the wire
A tool call is an ordinary tools/call. Its answer is a tool result with one plain-text content item, written for a model to read; read_asset with inline adds the file itself (an image, text or an embedded resource). There are no resource_links and no structuredContent; a program that wants the data as JSON uses the API door (The API).
POST /
Authorization: Bearer <access token>
Content-Type: application/json
Accept: application/json, text/event-stream
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": { "name": "create_concept", "arguments": {
"bundle": "01M3…", "path": "README",
"type": "Guide", "title": "Research",
"body": "# Research\nWhat this bundle is for.",
"change_description": "Said what this bundle is for" } } }
200 OK
{ "jsonrpc": "2.0", "id": 7, "result": {
"isError": false,
"content": [
{ "type": "text", "text": "ok README created, rev 1" }
] } }A call that fails answers the same way with isError: true and the text <code>: <message>. Only transport and account problems are JSON-RPC or HTTP errors: 401 without a valid token, and 403 (-32003) for a credential this address does not take or a blocked account (the list).
Every op
Every op below is served at both doors: POST /v1/<op> on the API, a tool of the same name over MCP, with the same arguments, rules and limits. Parameters with ? are optional. The input schemas (GET /v1, tools/list) carry the same fields with a description each; the manual topics explain the behaviour. manual is MCP only.
Field shapes
| Field | Shape | Meaning |
|---|---|---|
bundle | string | A bundle id (a 26-character ULID) from list_bundles, or the exact name of a bundle you can open (case and outer spaces aside). No bundle of that name answers not_found, two or more ambiguous_bundle: pass the id. |
path | string | Bundle-relative, segments of A–Z a–z 0–9 . _ - starting with a letter or digit, / between, no .md, depth ≤ 10. Umlauts are spelled out (ä → ae), other characters become -. A last segment named index or log is refused. |
type | string | Free text: Decision, Runbook, Guide, … |
status | string | stable | draft | deprecated by convention; left out reads as stable. |
tags | string[] | At most 32. Replaces all tags when given. |
relations | [{type, to}] | type = a relation type of the bundle, to = a path in it (may not exist yet). Replaces all relations when given. |
extra | object | Any other frontmatter keys. Replaces all of them when given. |
edits | [{old_str, new_str}] | update only, instead of body: up to 50 find-and-replace steps in order. Each old_str must occur exactly once in the body, else edit_mismatch and nothing is written. Needs rev. |
set_extra | object | update only, instead of extra: keys merged over the stored ones; null deletes a key. Needs rev. |
folder | string | A folder path, e.g. auth. Left out: the bundle root. |
rev | integer | From read_concept. The update fails with rev_mismatch if the concept changed since. |
change_description | string | One line, what changed and why, at most 200 characters. Required on every change to a concept: create, update, delete, move, a relation, an asset; a batch takes one for all or one per item. It is the log. |
since | string | ISO 8601 UTC, e.g. 2026-09-19T10:00:00Z. |
rights | string[] | Any of create, read, update, delete. |
cursor | string | Opaque. A page with more ends its text with a line cursor <token> (search_concepts, query); pass it back with the same filters. |
Reading readOnly · idempotent · every access, inside its rules
| Tool | Parameters and effect |
|---|---|
manualHow Korpus works | topic?The manual, by topic. One topic, or all of them with no argument. |
list_bundlesList bundles | activity?The bundles you see, with your role and how many concepts each holds. activity: also what is new in each since your last look, and your @-mentions. |
get_bundleShow bundle | bundleOne bundle whole: its state, relation vocabulary, collections and views, and how many people it is shared with. |
get_usageShow usage | shares?Your plan and how full your space is: documents and files, each as a share and in bytes, and the bundles you own as a count. Never refused. shares: also each bundle charged to you with its share of document space. |
list_folderList folder | bundle, folder?, recursive?, cursor?, limit?One folder: its concepts (path, type, title, description) and subfolders, a page at a time (a long folder ends with a cursor). recursive: every concept under it, flat, paged the same way. The way to find things; start here. |
read_indexRead folder index | bundle, folder?The folder's index.md: its concepts' titles and descriptions by type, and its subfolders. Generated from the concepts, never stored. A folder of more than 500 entries shows the first 500 and ends with a more: line naming the list_folder cursor for the rest. |
read_logRead change log | bundle, path?, since?, limit?, cursor?, titles?The log.md of a folder (every change under it), a concept (its own) or the bundle: kind, what changed, who, newest first. A long read stops with a cursor: pass it back for older changes. Generated from the change records, never stored. |
search_conceptsSearch concepts | bundle, q?, regex?, case_sensitive?, body?, digest?, type?, tag?, tags?, status?, stale?, stale_within_days?, date_from?, date_to?, prefix?, limit?, cursor?Search paths, titles, descriptions and tags, ignoring case and umlauts. body: also the text, quoting the line that hit. digest: a line under each hit saying what it is about. tags: all of them. stale_within_days: what falls due. date_from/date_to: on the concept's date. Pages of 50, up to 200. |
read_conceptRead concept | bundle, path, targets?A whole concept as markdown with its rev, what links to it (relations and body links), and who its body @-mentions. targets: also the title and type of everything it points at or is pointed at from. |
read_conceptsRead concepts | bundle, pathsUp to 50 concepts in one call, each as read_concept shows it. The answer holds at most 2.5 MB; concepts past that come back as left, to read in another call. |
read_assetRead asset file | bundle, path, inline?An asset's file: a download URL valid 60 seconds, or with inline the file itself in the answer (up to 3 MB; images shown, text read). Read its concept first; it usually answers. |
list_activityShow recent activity | bundle?, since?, mentions_only?Without bundle: what is new in each bundle since your last visit, and where you were @-mentioned. With bundle: its latest 50 changes. since: only what came after it; without bundle, counted from the later of since and your last visit. mentions_only: only your @-mentions. |
list_relation_typesList relation types | bundleThe bundle's link vocabulary, and the built-in links_to the server derives from body links. Read it before writing a relation. |
get_schemaShow schema | bundleThe bundle's collections (folders of items sharing one shape: item type, fields, key, item count) and saved views, then the concept types no collection declares. Read it before writing many similar docs. |
get_collectionShow collection | bundle, nameOne collection: its item type, fields with counts per enum value, required sections, views, item count, and a blank item to fill. |
queryQuery items | bundle, view?, from?, type?, where?, q?, tags?, status?, stale?, prefix?, sort?, group_by?, aggregates?, fields?, limit?, cursor?Items of one or more collections as rows: path, title and fields, no bodies, a ref shown with its title. A saved view, or filters by any field, sorted by up to three; across collections the core fields (date, status, stale_after, owner, title). Or grouped (group_by), or totals (aggregates: count, sum, avg, min, max); the answer's mode says which. type: a type no collection declares, read as stored. Sees every write that committed. |
check_relationsCheck relations | bundleRelations, body links and ref fields pointing at concepts that do not exist, relations naming a type the bundle does not declare, and relation types nothing uses. |
get_rightsShow my rights | bundle, pathWhat you may do at one path, and which of your rules say so. |
list_delegationsList delegated access | me, then your delegations: the AI agents, automations and programs acting for you, each with its job and rules. A delegation sees only itself. |
list_connectionsList connections | name?Every app connected as me or a delegation (or, with name, as that one): sign-ins through the browser or by device code, keys and trusts, each with its id, door and method. Me only. |
list_received_delegationsList delegations to you | Delegations other people made to you, sharing their bundles: who, its rules, pending or accepted. Me only. |
list_publicationsList publications | bundleEvery version a bundle of yours published, its last ending, and the copy of the one still filling. Me only. |
list_templatesList templates | category?, tags?, cursor?, limit?The catalog: published templates, the newest version of each, narrowed by category and tags. Me only. |
get_templateShow template | idOne template: its listing, publisher and size. Me only. |
get_connectGet connect recipes | name, bundle?How to connect an app as me or one delegation, in the order to prefer: sign-in through the browser at its MCP address (me from Claude's connector directory, any access as a custom connector or a claude mcp add line); for me also korpus login, the CLI signed in through the browser; for a delegation then a device code for a box with no browser, a trusted AWS role or GitHub repository for a workload, and a key last. Also a CLAUDE.md block with a SessionStart hook so every session reads the bundle first (given bundle, both name it). Me only. |
Writing asks · every access, inside its rules
| Tool | Parameters and effect |
|---|---|
create_conceptCreate concept | bundle, path?, type, body, title?, description?, tags?, relations?, status?, stale_after?, date?, extra?, change_descriptionA new concept. Refused if the path is taken. A path with umlauts or spaces is made valid (schäfer → schaefer) and the answer names it. An item of a collection may leave path out: its key makes it. Every write to a collection is checked against it. |
update_conceptUpdate concept | bundle, path, …fields?, edits?, set_extra?, rev?, change_descriptionChange the fields you send; the rest stay. edits change part of the body (find-and-replace), set_extra some frontmatter keys. Pass the rev you read to fail on a concurrent change; required when sending tags, relations, extra, edits or set_extra. |
delete_conceptDelete concept | bundle, path, change_descriptionDelete one concept. An item others name by a ref field follows its on_delete: restrict refuses, cascade deletes them too, keep leaves the ref dangling. |
create_assetCreate asset | bundle, path, filename, body, bytes + content_type | text, source?, title?, description?, tags?, relations?, …, change_descriptionA file (pdf, zip, docx, csv…) and the concept that explains it, at one path. bytes: the answer is one curl line that uploads it from a shell (100 MB at most). text: a small text file (up to 1 MB) stored at once. body is required. |
update_assetReplace asset file | bundle, path, rev, filename, bytes + content_type | text, …fields?, change_descriptionA new file for an asset, and any document fields sent, in one write. The old file is deleted once it commits. |
delete_assetDelete asset | bundle, path, change_descriptionDelete an asset: its file and its concept. Refused on a concept with no file. |
create_conceptsCreate concepts | bundle, concepts, change_description?Up to 50 new concepts in one call, each as create_concept takes it. Each is its own write with its own answer line. |
update_conceptsUpdate concepts | bundle, updates, change_description?Up to 50 changes in one call, each as update_concept takes it. |
delete_conceptsDelete concepts | bundle, paths, change_descriptionDelete up to 50 concepts in one call. |
move_conceptMove concept | bundle, from, to, rev?, update_links?, change_descriptionMove or rename a concept; its body, frontmatter, tags and history go with it. Every concept linking to it is rewritten to the new path, unless update_links is false. |
move_conceptsMove concepts | bundle, moves, update_links?, change_descriptionUp to 50 moves in order, one per concept: a folder moves as one move for each concept in it. |
add_relationAdd relation | bundle, from, type, to, change_descriptionLink one concept to another, keeping its other links; reads from <type> to, and the answer echoes it. Nothing is written if the link is there. |
remove_relationRemove relation | bundle, from, type, to, change_descriptionRemove one link, keeping the others. |
Managing asks · me only
| Tool | Parameters and effect |
|---|---|
create_bundleCreate bundle | name, description?A new bundle you own, ready at once, with a starter set of relation types (relates_to, depends_on, supersedes, contains, precedes). |
create_relation_typeDefine relation type | bundle, name, label, kind, inverse_label?, description?Declare or replace one kind of link: symmetric, directed (needs inverse_label) or unidirectional, with a line on when to use it. |
delete_relation_typeRemove relation type | bundle, nameRemove one relation type; edges using it stay and check_relations lists them. |
define_collectionDefine collection | bundle, name, item_type, folder, key?, fields?, sections?, title?, description?Declare a collection: a flat folder whose items all have one item type, fields (string, number, date, datetime, bool, enum, list, ref, refs) and required sections, and a key their path is made from. Every write is checked against it. What is already there is checked; one that does not fit is named and nothing is declared. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
update_collectionChange collection | bundle, name, folder?, fields?, sections?, default_view?, title?, description?Change a collection: fields replaces the whole list, a new folder moves every item there. Every item is checked again; one that does not fit refuses the change. name, item_type and key never change. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
delete_collectionRemove collection | bundle, nameRemove a collection. Its items stay as they are. Refused while a view or a ref field names it. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
define_viewSave view | bundle, name, from, display?, where?, sort?, group_by?, aggregates?, columns?, title?, description?Save a view of one or more collections: filter, sort, group, totals, and how it shows (table, list, board, tree). The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
update_viewChange view | bundle, name, from?, display?, where?, sort?, group_by?, aggregates?, columns?, title?, description?Change a saved view: each key sent replaces its value, null clears it. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
delete_viewRemove view | bundle, nameRemove a saved view. Refused while a collection opens on it. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
update_bundleRename bundle | bundle, name?, description?, relation_types?Change a bundle's name, description or whole relation vocabulary (relation_types replaces the list). Name and description: owner only. The vocabulary: the owner or a contributor; a share needs create, update and delete on the whole bundle (*). |
delete_bundleDelete bundle | bundle, confirmDelete a bundle you own with everything in it, for good. confirm is its name, typed back. |
export_bundleExport bundle | bundleThe bundle as an OKF folder in a zip, as a download link valid 15 minutes. |
accept_delegationAccept delegation | from, nameAccept a delegation waiting for you: its bundles join your list, as far as its rules go. Me only. |
leave_delegationLeave delegation | from, nameDecline a pending delegation to you, or leave an accepted one. Your writes stay. Me only. |
clone_templateUse template | idTake a template: a new bundle of yours, filled from the published version and counted against your space. Me only. |
end_publicationEnd my publication | publicationAs its publisher: withdraw a pending version, remove a published one, or dismiss one a reviewer ended. Deletes the version; clones stay. Me only. |
create_delegationCreate delegation | name?, description?, rules?, person?Prepare a delegation — an AI agent, an automation or a program acting for you — with its job and the least rules the job needs. It works at either door, MCP and the API; it signs in through the browser, or last holds a key. With person (@username or an email address) it is a share with another Korpus user instead: its rules name only bundles you own, they accept first, and they may connect their own apps within it but cannot share it on. name is required unless person is given. |
update_delegationUpdate delegation | name, description?, rules?Change a delegation; rules replace all of its rules. Holds from its next request. Not me. |
delete_delegationDelete delegation | nameDelete a delegation and end every connection it holds: sign-ins, keys and trusts. Not me. |
add_delegation_ruleAdd delegation rule | name, bundle, path, rightsAdd one rule; the same bundle and path merge their rights. Not me. |
remove_delegation_ruleRemove delegation rule | name, bundle, pathRemove one rule, named exactly as list_delegations prints it. Not me. |
revoke_connectionRevoke connection | name, idEnd one connection of me or a delegation at once: a sign-in, a key or a trust. id: as list_connections prints it, or its first 8 characters as a log shows a key. |
create_keyCreate key | name, label, days?, door?A key (kp_…) for one delegation at one door (api, the default, or mcp): a bearer for a host that can only send a fixed header, or a client_credentials secret for one that fetches its own tokens. A key unused for 90 days stops working. Shown once; expires after 30 (the default), 90 or 365 days. Never for me. |
add_trustTrust a workload | name, provider, issuer?, role_arns?, repository?, environment?, ref?, sub_pattern?, repository_id?, repository_owner_id?Let AWS roles (provider aws: your account issuer and role ARNs) or a GitHub repository (provider github: owner/name, pinned to an environment, a ref or a written sub_pattern) act as a delegation with no secret. The workload trades its platform token at /oauth/token for a one-hour token at the API door. Answers the setup. Never for me. |
send_feedbackSend feedback | kind, severity, title, goal, expected, actual, steps, error?, bundle_id?, paths?, workaround?, client?Your agent files a ticket with Korpus support: a bug, something confusing, a missing feature, an idea. It writes what you wanted, what it expected, what happened and the calls that reproduce it; the answer comes to your account email. At most 5 an hour, never counted against your usage. Me only. |
Only on the API
These ops are served at the API door alone: the web app's own, and a few a program signed in as you may call. who says who is offered each: you, signed in (your own sign-in, never a delegation), the web app (its own session alone: account, billing, support) or no sign-in (the cancel page, which needs no credential). A delegation sees none of them. Questions about billing: support@korpus.cloud.
| Op | Parameters and effect |
|---|---|
get_meyou, signed in | Who you are, your plan (free, plus or business, the word get_usage says), billing, theme and Terms, and the bundles deleting you would take with it. |
mark_seenyou, signed in | bundleYou have seen the bundle as it is now: its "new" count starts over. |
accept_termsthe web app | version, username?Agree to the Terms in force; a version that is not current is refused. A first Google sign-up also sends the username it chose. |
set_themethe web app | themeLight, dark or auto: the copy of the choice that travels between devices. |
change_emailthe web app | emailStart an email change: the address must be free, then a code is mailed to it. |
sync_emailthe web app | After the code is confirmed: move the account to the address the pool verified. |
find_peoplethe web app | prefixThe accounts whose username or address starts with prefix, for the share field. Only on an organisation's own deployment; elsewhere nobody. |
list_sessionsthe web app | The browsers signed in as you, most recently used first. |
clear_sessionsthe web app | Forget every session row, so the list does not name browsers whose tokens no longer work. |
delete_accountthe web app | confirmDelete the account and everything it owns. confirm repeats the username. |
send_supportthe web app | topic, message, context?Write to the operator; the reply goes to your account's address. Five a clock hour. |
create_checkoutthe web app | Start the payment page for Plus; answers its url. Refused while Plus is live. |
sync_billingthe web app | After checkout: read the subscriptions fresh and record the deciding one. |
cancel_subscriptionthe web app | End Plus at the end of the paid period. |
resume_subscriptionthe web app | Take back a pending cancellation of Plus. |
create_billing_portalthe web app | The payment provider's own page for card and invoices; answers its url. |
wakeno sign-in | Start the database if it paused after idle; answers ready. ready false: it is starting, ask again in a few seconds. |
cancelno sign-in | name?, email?, kind?, reason?Cancel Plus, withdraw from the contract, or cancel for cause, without signing in. The receipt goes to the given address; it answers 202 whatever it found. Only where Plus is sold: elsewhere unknown_op. |
cancel_confirmno sign-in | tokenThe withdrawal link from the receipt: refunds and ends Plus now. Only where Plus is sold. |
Files
An asset is a file (a PDF, a spreadsheet, an archive) and the concept that explains it, at one path: type: asset, with an asset block the server writes (filename, content type, size, source, whether it has landed). The body is required. It says what is in the file, so the next agent reads two kilobytes of prose instead of parsing forty megabytes again. Moving or deleting the concept moves or deletes the file; whoever may read the concept may download it.
- Upload by URL:
create_asset(over MCP or the API) withbytesandcontent_typeanswers one line for a shell:curl -fsS -T <file> -H 'content-type: …' "$(curl -fsS https://mcp.fra.korpus.cloud/u/<id>)". The link is good for 15 minutes; the content type and length are signed, so send exactly what you declared. The file goes straight to storage, up to 100 MB. - Upload as text:
textinstead ofbytesstores a small UTF-8 file (a CSV, JSON or Markdown, up to 1 MB) at once, for an agent with no shell. - Download:
read_assetanswers a URL valid for 60 seconds; withinlinethe file itself comes back in the answer (up to 3 MB): an image as image content, text as text, anything else as an embedded resource. - Replace:
update_assetwith therevyou read takes a new file, and any document fields, in one write; the old file is deleted once it commits.
Files count against their own space, apart from the document space, charged to whoever created the bundle (quota_exceeded, limit asset_space). In the web app, drop a file on a folder.
Resources and prompts
| URI | What |
|---|---|
korpus://bundle/{bundle_id} | A bundle: name, your role, count, description and its root folder. Listed by resources/list. |
korpus://bundle/{bundle_id}/{+path} | A concept: its rev and markdown. A template; concepts are not listed. |
Both are text/markdown and go through the same access checks as the tools.
| Prompt | What it does |
|---|---|
whats_in_korpus | What's in Korpus: Lists your bundles, then the index of each. Reads only. |
save_session | Save what we learned: Reads what is there, proposes what to write, waits for your OK, then writes it, saying what changed. |
whats_new | What's new: Changes since your last visit, and where you were mentioned. Reads only. |
ask_korpus | Answer from Korpus: Answers your question from your bundles, naming the concepts it rests on. Reads only. |
check_bundle | Check a bundle: Finds dangling relations, unused relation types, stale concepts and old drafts, and proposes a fix for each. Writes nothing until you pick. |
start_bundle | Start a new bundle: Plans a bundle with you, waits for your OK, then creates it with a README and AGENTS. Me only. |
Errors
Over MCP a failure reads <code>: <message>, sometimes followed by details as JSON and a pointer to a manual topic; at the API it is {"error": {"code", "message", …details}}, with the HTTP status the failure has everywhere in Korpus. The message names the fix.
| Code | Meaning |
|---|---|
rev_mismatch | The concept changed since you read it. Read again, merge, update with the new rev the message names. |
rev_required | An update sent tags, relations, extra, edits or set_extra without rev. Read, merge, pass rev. details: current_rev |
edit_mismatch | An old_str in edits is not in the body exactly once; nothing was written. Copy it from a fresh read, widen it until unique. details: index, matches |
change_description_required | A create, update or delete said nothing about what changed. One line, at most 200 characters. |
not_found | Not there, or not yours to see. Not a permission hint. |
restricted | The delegation's rules refuse it. The message lists what it may do. Do not retry; the person widens the rules. |
rate_limited | Far too many writes or reads in a short time: a loop gone wild, not normal work. Nothing ran. details: limit (writes, reads), retry_after (seconds) |
quota_exceeded | Out of room or over a count. details: limit (space for documents, asset_space for files; publications is web only), max? |
maintenance | An upgrade is running behind a short gate (minutes at most). Wait retry_after seconds and send the call again. HTTP 503. |
database_starting | The database paused after idle and is starting (about 20 s). Nothing ran: wait retry_after seconds and send the call again; the SDK and the korpus command do so by themselves, within wakeWait (30 s). HTTP 503 with Retry-After. |
published_read_only | A published catalog version (web only), which nobody writes. Edit its source bundle instead. |
account_blocked | The account is blocked. Every request is refused. |
internal_error | Our fault. Retry once, then have your agent send_feedback; it quotes the ref, which leads us to the log line. |
support_rate_limited | More than 5 support messages this hour (send_feedback and the contact form together). details: retry_after (seconds) |
More codes
invalid_path, invalid_concept, invalid_frontmatter, invalid_name | A path, field or name does not fit its rules (charset, depth, a last segment named index or log). |
invalid_access, invalid_delegation | A delegation, key or rule is malformed: a name, job, key label or door, a pattern's charset, an unknown right, a .md in a pattern. Also me, which is never changed and never holds a key or a trust. |
delegation_not_found, delegation_exists, connection_not_found | No delegation of that name, a delegation of that name already exists, or no connection with that id. |
invalid_trust, role_claimed | A workload trust does not fit (the message names the field), or the AWS role already acts as another of your delegations. |
invalid_relation_type | The relation type is not declared in this bundle: list_relation_types, then create_relation_type. |
invalid_cursor, invalid_regex, invalid_query | Start the search again without the cursor; the regex uses a construct outside the accepted subset; a filter such as date_from is not a day. |
not_allowed_here, wrong_type, wrong_path, item_mismatch | A concept does not fit its collection: in a collection folder that is not its item, under _schema, the wrong type, the wrong path (expected_path names the right one) or a field or section missing or of the wrong kind. Every problem is named at once. |
invalid_collection, collection_not_found, still_referenced | A collection definition or change is malformed, or items already there do not fit (named); no collection of that name; or a delete refused because items point at the concept with on_delete restrict (named). |
invalid_view, view_not_found | A view definition is malformed (a field across collections that is not a core field, a board without one group_by), or no view of that name. invalid_query also answers a query that cannot run (an unknown field, an op or aggregate that does not fit its kind). |
body_too_large, too_many_tags, too_many_relations, too_many_links, export_too_large | Over a ceiling; see Limits. too_many_links: the body links more concepts than one may; split it. |
invalid_items, invalid_move | A batch list refused before anything ran (empty, too long, too many bytes, one path twice), or a move from a path to itself. |
confirm_mismatch, is_version, in_review | delete_bundle: confirm is not the bundle name; the bundle is a catalog version (ended in the web app); a version of it is in review. |
not_an_asset, invalid_asset | The concept has no file, or the filename, content type, size or text of one is wrong, or its upload has not landed yet. |
ambiguous_bundle | A bundle named by a name two or more of your bundles carry. Pass the id from list_bundles (details: ids), or rename one so each name is unique. |
bundle_not_empty, conflict | The state does not allow it right now, e.g. a create at a path already taken. |
forbidden | Your role in the bundle does not allow it, e.g. a viewer writing or a contributor sharing. |
user_not_found, email_taken, username_taken | People: nobody with that name or address (a share, on a deployment that lists its people), or a name or address already taken. |
Before a tool runs
Refusals of the request itself, before any tool: over MCP a JSON-RPC error -32003 with the HTTP status, at the API {"error": {"code", "message"}}.
| Code | Meaning |
|---|---|
wrong_audience403 · MCP, API | A credential away from the door and address it was made for: another delegation's or another account's address, a delegation at /, or the API instead of MCP (or back). The message and details.url name where it works. |
account_blocked403 · MCP, API | The account is blocked. Every request is refused. |
unauthorized401 · API | No token, or one that is unknown, expired or revoked. |
unknown_op404 · API | No op of that name for this caller (or not at this door). GET /v1 lists them. |
invalid_arguments400 · API | The body is not JSON, or does not fit the op's input schema; the message names each field. Nothing ran. |
method_not_allowed405 · API | GET /v1 and POST /v1/<op> only. |
The codes are the same on every door and in the SDK's KorpusError. Retry hints: rate_limited, maintenance and database_starting carry retry_after, in seconds. A rate_limited wait is seconds: wait it out, then go on where you stopped. manual topic=errors explains every code.
Limits and usage
| Limit | Value |
|---|---|
| Concept body | 300 KB |
| File (asset) | 100 MB; 1 MB sent as text, 3 MB read inline |
| Upload link / download URL | 15 minutes / 60 seconds |
| change_description | 200 characters |
| Path | depth 10, 900 bytes |
| Tags per concept | 32 |
| Relations per concept | 32 |
| Concepts one body links to | 200 |
| Items per batch call | 50, bodies 2 MB in all |
| read_concepts answer | 2.5 MB |
| Relation types per bundle | 16 |
| Collections per bundle | 16 |
| Fields per collection | 24 |
| Views per bundle | 32 |
| Enum values per field | 50, each 60 characters |
| Items in a list field | 50 |
| query | 8 collections, 10 conditions, 50 values per in, 3 sort keys, 8 aggregates, 1,000 groups, 200 rows a page |
| Search page | 50, up to 200 |
| Regex length | 200 characters |
| Delegations per account / rules per delegation | 50 / 50 |
| Keys per delegation | 5, each 30, 90 or 365 days; off after 90 idle days |
| Trusts per delegation | 10 |
| Export | 50 MB of concepts and files; link 15 minutes |
Space is the one quota: counted per person, over the bundles they created, for documents and for files apart. A concept counts its body and fields (title, description, tags, relations, frontmatter), plus 1 KB. Full, a write answers quota_exceeded; get_usage shows how full each is, and is never refused. Writes and reads each have a short-term rate, there only to stop a runaway loop: a call counts 1, a batch 1 per item, a body search or an export 10 reads. Over one, the call answers rate_limited with retry_after in seconds. The rates are higher on Plus and are not published.
The SDK
@korpus-cloud/sdk is Korpus from code: a CI job, a cron script, an app of yours reads and writes the same bundles people and AI apps use, as a delegation held to the rules you give it. Every method is one op of the API.
| Package | @korpus-cloud/sdk on npm. MIT, ESM, no dependencies. Node 20 or later, Bun or Deno. Server side only. |
|---|---|
| Who | A delegation. A sign-in as you (me) is not what the SDK is for, and a key or a trust never belongs to me. |
| Credential | new Korpus() finds one at its first call: KORPUS_KEY first, then a GitHub Actions job with KORPUS_AUDIENCE (traded for a one-hour token, renewed before it ends), then a sign-in made on this machine with korpus login, then an AWS workload with KORPUS_AUDIENCE (traded the same way); with none, the call throws no_credential. To choose: new Korpus({ credentials: fromGitHubActions({ audience }) }), or fromAwsWebIdentity, fromOAuth, fromStaticToken. How to get each: Connect. |
| Which Korpus | The baseUrl option; else KORPUS_URL; else the deployment the key names (kp_fra_… is fra.korpus.cloud, kp_dev.fra_… is dev.fra.korpus.cloud); else the one KORPUS_AUDIENCE names; else https://api.fra.korpus.cloud. Any URL of a deployment will do. A key names its deployment and is sent there unless you give another URL. |
Use
import { Korpus } from '@korpus-cloud/sdk';
const korpus = new Korpus(); // KORPUS_KEY, a trusted workload, or a sign-in from korpus login
const notes = korpus.bundle('01JBQ3M7X9VK2NDCTF8ZPYW4RH');
// Find, read, change.
const entries = await notes.folder('runbooks');
const { concept } = await notes.read('runbooks/deploy');
await notes.update('runbooks/deploy', {
rev: concept.rev, // fails with rev_mismatch if someone changed it since
body: concept.body + '\n- Check the dashboard after the deploy.',
change_description: 'Added the post-deploy check',
});
// Many at once: each item answers on its own.
const items = await notes.createMany(
[
{ path: 'log/2026-10-22', type: 'Log', body: 'Nightly sync: 14 records.' },
{ path: 'log/2026-10-23', type: 'Log', body: 'Nightly sync: 9 records.' },
],
'Nightly sync results',
);
for (const item of items.items) if (item.status !== 'ok') console.error(item.path, item.error.message);Every method is one op. korpus.call(name, args) reaches any of them by name, typed, with the text a model would have read beside the data:
const { data, text } = await korpus.call('check_relations', { bundle: notes.id });Every change takes a change_description: one line, what changed and why. It is the log; nothing else records why a concept moved. Rules, paths and fields are those of the ops: Every op has each parameter, Delegations and rights the rules.
Every method
korpus is a new Korpus(); bundle is korpus.bundle(id), with the bundle id from listBundles(). why is the change description.
| Method, tool and effect |
|---|
korpus.describe()GET /v1What this credential may call: the API version, the delegation, its rules, and every tool with its input schema. |
korpus.wake() / awaitReady()wakeStart the database if it paused after idle, sending no credential: {ready}. awaitReady asks until it is ready, within wakeWait. A call needs neither: it waits by itself. |
korpus.listBundles({activity?})list_bundlesThe bundles the delegation sees; with activity, what is new in each. |
bundle.info()get_bundleThe bundle whole: state, relation vocabulary, collections and views. |
korpus.usage()get_usageHow full your document and file space is. Never refused. shares: true adds each bundle share. |
korpus.self()list_delegationsThe delegation itself: its job and rules. |
korpus.activity({since?, mentions_only?})list_activityWhat is new in every bundle it sees. |
bundle.folder(folder?, {recursive?, cursor?, limit?}) / folderPage(…)list_folderOne folder: its concepts and subfolders. The way to find things. folderPage also answers the cursor for the next page and the folder's README / AGENTS. |
bundle.index(folder?)read_indexThe folder's generated index, as markdown. |
bundle.log({path?, since?, limit?, cursor?}) / logPage({…, titles?})read_logThe changes under a folder or to a concept, newest first. logPage also answers the cursor to pass back for older changes, when you last looked, and with titles each row's concept by title. |
bundle.search(query)search_conceptsSearch paths, titles, descriptions, tags, and with body the text. |
bundle.read(path, {targets?}) / readMany(paths)read_concept · read_conceptsA concept with its rev and what links to it (with targets, the title and type of both ends of each edge); up to 50 at once. |
bundle.create(path, input) / createMany(concepts, why?)create_concept · create_conceptsA new concept; up to 50, each answering on its own. |
bundle.update(path, input) / updateMany(updates, why?)update_concept · update_conceptsChange the fields sent (edits and set_extra change part of body and extra); pass rev to fail on a concurrent change. |
bundle.delete(path, why) / deleteMany(paths, why)delete_concept · delete_conceptsDelete one concept, or up to 50. |
bundle.move(from, to, opts) / moveMany(moves, opts)move_concept · move_conceptsRename; every link to it is rewritten unless update_links is false. |
bundle.relate(from, type, to, why) / unrelate(…)add_relation · remove_relationAdd or remove one relation. |
bundle.relationTypes() / checkRelations()list_relation_types · check_relationsThe link vocabulary; links that point nowhere. |
bundle.schema() / collection(name)get_schema · get_collectionEvery collection with its item count, the saved views and the types no collection declares; or one collection, with counts per enum value, its views and a blank item. |
bundle.query({view} | {from, where?, sort?, group_by?, aggregates?, fields?, limit?, cursor?})queryItems as rows, by a saved view or by filters; or their groups or totals. mode says which: rows, groups or totals. |
bundle.defineCollection(input) / updateCollection(name, update) / deleteCollection(name), defineView / updateView / deleteViewdefine_collection · update_collection · delete_collection · define_view · update_view · delete_viewDefine, change or remove a collection or a saved view. Me only: a delegation is refused. The owner or a contributor; in a shared bundle, a share with that right on the whole bundle (*). |
bundle.rights(path) / activity()get_rights · list_activityWhat this token may do at a path; its latest changes. |
bundle.readAsset(path, {inline?}) / createAsset / updateAsset / deleteAsset(path, why)read_asset · create_asset · update_asset · delete_assetFiles, tool by tool: a download URL, or the upload URL to PUT to. |
bundle.uploadAsset(path, bytes, input)create_asset · update_asset · read_assetA file in one call: the tool, the PUT straight to storage, then the check that it landed. With rev it replaces the file. |
bundle.downloadAsset(path)read_assetThe file's bytes and its asset block: {asset, bytes}. |
Files
A file (an asset) is kept beside the concept that explains it, at one path; body says what is in it, so the next reader need not open the file. uploadAsset sends the bytes straight to storage, never through Korpus, up to 100 MB; with rev it replaces the file.
const pdf = await readFile('q3-board-pack.pdf');
await notes.uploadAsset('reports/q3-board-pack', pdf, {
filename: 'q3-board-pack.pdf',
content_type: 'application/pdf',
body: 'Q3 board pack. Revenue 1.2 M (+8 %), churn 2.1 %. Hiring freeze until Q1.',
change_description: 'Filed the Q3 board pack',
});
const { bytes } = await notes.downloadAsset('reports/q3-board-pack');Files count against their own space, apart from the space for documents, charged to whoever created the bundle (quota_exceeded, limit asset_space).
Collections and views
A collection is a folder of items that share one shape: a name (incidents), an item type (Incident), fields, required sections and a key the path is made from. The owner defines and changes it (defineCollection, updateCollection, as me), and every write of an item is checked against it (item_mismatch, wrong_path, wrong_type, not_allowed_here). A view is a saved query of one or more collections, shown as a table, list, board or tree (defineView). A delegation reads both and queries the items; a type no collection declares is queried too (type), by its values as stored.
const { collections } = await notes.schema();
const incidents = await notes.collection('incidents'); // fields, views, a blank item
const open = await notes.query({
from: ['incidents'],
where: [{ field: 'severity', op: 'in', value: ['high', 'critical'] }],
sort: [{ field: 'opened', dir: 'desc' }],
limit: 20,
});
if (open.mode === 'rows') for (const row of open.rows) console.log(row.path, row.fields);
const board = await notes.query({ view: 'open-incidents' }); // a saved viewConditions: eq, ne, lt, lte, gt, gte, in, prefix, contains, exists, up to 10, all of which must hold. Sort by up to three fields; path ends the order. Several collections read together take only the core fields (date, status, stale_after, owner, title). group_by and aggregates answer groups or totals instead of rows. The answer's mode says which (rows, groups, totals); rows carry a cursor when there is more.
Errors and retries
A refusal throws KorpusError: status (the HTTP status; 0 when no answer came), code (stable: restricted, not_found, rev_mismatch, rate_limited, …), message (what to do, for a person), retryAfter in seconds where the server names a wait, and details. The codes are those of Errors; a key of another deployment is refused with 401 (give its URL, or let the key name it).
import { KorpusError } from '@korpus-cloud/sdk';
try {
await notes.update('runbooks/deploy', { rev: 4, body, change_description: 'Rewrote step 3' });
} catch (error) {
if (error instanceof KorpusError && error.code === 'rev_mismatch') {
// read again, merge, update with the new rev
} else throw error;
}Writes and reads have the same short-term rates as on every way into Korpus. Over one the call is refused with rate_limited and retryAfter in seconds. new Korpus({ retries: 2 }) waits that long and sends it again. korpus.usage() says how full your document and file space is and is never refused. The ceilings every plan shares: Limits and usage.
The database pauses after a while idle. A call that meets it starting is refused with database_starting before anything ran, so the client waits retryAfter and sends the same call again, by itself, for up to wakeWait seconds all told (30 by default); an upgrade's maintenance gate is waited out alike. korpus.awaitReady() warms it up ahead of time.
Options
new Korpus(options), every one optional:
token | A delegation's key (kp_…), sent as the bearer. It names its deployment, so it needs no baseUrl, and is sent there unless baseUrl or KORPUS_URL names another URL. Default: KORPUS_KEY. |
credentials | Where the bearer comes from: fromGitHubActions({audience}), fromAwsWebIdentity({audience}), fromOAuth(), fromStaticToken(key). Default, in order: KORPUS_KEY; a GitHub Actions job with KORPUS_AUDIENCE (a trusted workload); the sign-in korpus login stored on this machine; an AWS workload with KORPUS_AUDIENCE. |
delegation | Which stored sign-in, when this machine holds several. Default: KORPUS_DELEGATION. |
baseUrl | Which Korpus: any URL of the deployment (the app, its API, an MCP address); the ops are called at its API host, https://api.<host>/v1. Default, in order: KORPUS_URL; the deployment the key names; the deployment KORPUS_AUDIENCE names; https://api.fra.korpus.cloud. A platform token is traded only at the deployment its audience names. |
retries | How many times a call refused with rate_limited is sent again, after the wait the server names. Default 0: it throws at once. |
maxRetryWait | The longest wait a retry accepts, in seconds. Default 60. |
wakeWait | How long a call waits, in seconds all told, while the database starts (database_starting) or an upgrade holds every call (maintenance): nothing ran, so it sends the same call again after the Retry-After the server names. Default 30; 0 throws at once. |
fetch | A fetch of your own: a proxy, a test double. |
userAgent | Added to the user agent, e.g. nightly-sync/1.2. |
The variables: KORPUS_KEY (a delegation's key, which names its deployment), KORPUS_AUDIENCE (a trusted workload's audience), KORPUS_DELEGATION (which stored sign-in) and KORPUS_URL (which Korpus, over what the key or the audience names).
What a delegation cannot do
It is held to its rules: it works inside the bundles and paths they name, and listings leave out what it may not read. Creating, sharing, exporting or deleting bundles, defining collections and views and managing delegations stay with you: in the web app, as me over MCP, or with the korpus command.
The CLI
The SDK ships one command, korpus. Install it once: npm install -g @korpus-cloud/sdk.
As you
korpus login [--url <url>] korpus logout
korpus login alone opens Korpus in your browser: you allow Korpus CLI to use Korpus as you, and the browser comes back to the command on 127.0.0.1. Every other command then runs as you, with your own permissions, so it can do what a delegation cannot: make and share bundles, and make delegations with their rules, keys and trusts. It never uses KORPUS_KEY, and a program's new Korpus() never picks this sign-in up. It is a connection of me on the Access page, where you revoke it.
korpus bundle list | create <name> | show | update | delete <bundle> --confirm <name> korpus schema <bundle> korpus collection list <bundle> | show <bundle> <collection> korpus delegation list | create <name> [--person <@user|email>] [--rule <bundle>:<path>:<rights>]... | allow | deny | update | delete korpus delegation received | accept <@from> <name> | leave <@from> <name> korpus key create <delegation> --label <what for> [--days 30|90|365] [--door api|mcp] korpus trust github <delegation> --repo <owner/name> --environment <env> korpus trust aws <delegation> --issuer <url> --role-arn <arn> korpus connection list [<access>] | revoke <access> <id> korpus whoami korpus call <tool> ['<json arguments>']
Concepts go in and out as Markdown files, laid out like an export: <path>.md, YAML frontmatter, then the body. type, title, description, tags, relations, status, stale_after and date are the concept's fields; any other key is kept as written. korpus read adds rev, the revision it read, and korpus update sends each file's rev back, so it fails with rev_mismatch if someone changed the concept since; --rev overrides it for one file.
korpus ls <bundle> [<folder>] [-r] korpus read <bundle> <path> [-o <file.md> | --out <dir>] korpus create <bundle> <file.md>... -m <why> [--root <dir> | --path <path>] [--type <type>] korpus update <bundle> <file.md>... -m <why> [--root <dir> | --path <path>] [--rev <n>] korpus delete <bundle> <path>... -m <why>
A file's concept path is the file's path under --root without .md, or --path. Several files go as one batch per 50. Every command prints what the tool answered, or with --json its data; a new key alone goes to stdout. A database still starting is waited out as the SDK does (wakeWait). The deployment: --url, else KORPUS_URL, else the one you signed in to, else production. korpus help lists every command.
As a delegation
korpus login <delegation> [--user <username>] [--url <url>] korpus logout <delegation> [--user <username>] [--url <url>]
korpus login <delegation> signs this machine in as the delegation by device code (Connect): it prints a short code and a link, you allow it in Korpus, and the sign-in is stored in ~/.config/korpus/sign-ins.json (owner only; $XDG_CONFIG_HOME if set), refreshed at the deployment's API as it ages. new Korpus() finds it; with several stored, name one with KORPUS_DELEGATION or the delegation option. The sign-in is a connection on the Access page, where you revoke it. korpus logout forgets it on this machine. --url names any URL of the deployment when it is not production; a bare host (dev.fra.korpus.cloud) is taken as https://. A login that waits for your yes keeps asking through a dropped network until the code expires.
A sign-in made here serves the API, where the SDK calls. An MCP client such as Claude Code signs in at the delegation's MCP address itself, or sends a key.
Not on the API
Publishing a bundle to the catalog, reviewing it and the users admin are for reviewers, in the web app or as a reviewer's me; they are not listed here. Billing is the web app's own session's, listed under Only on the API. Questions: support@korpus.cloud.