Docs › Reference

For everyone

The HTTP API

Every endpoint the browser calls, grouped by the screen that calls it — with its parameters, what it answers and the permission it needs.

Everything the browser client does, it does through this API. There is no second surface: static/app.js is a static file that talks to the endpoints below, which means every screen can be rebuilt from this document, and every endpoint a screen uses is documented under that screen.

210 endpoints: 95 GET, 64 POST, 25 PUT, 5 PATCH, 21 DELETE. All of them appear below. The ones no screen calls are under Not called by any screen at the end; they are integration and administrative endpoints and matter to an integrator.

How authentication works

SESSION_COOKIE = "krisid_session". SESSION_HOURS = 12, and it is an idle timeout: every request that lands slides the expiry forward twelve hours (_read_session).

A session is created in exactly three places, and each one sets the cookie on the response:

How Endpoint
Signing in with a username and password POST /api/session
Accepting an invite and setting a password POST /api/invites/{token}/accept
Confirming a self-sign-up link POST /api/signup/{token}

new_session inserts a user_session row holding the SHA of the token, the user id, expires_at = now + 12 hours and the first 200 characters of the User-Agent. The token itself is never stored.

The cookie is set by _cookie:

Set-Cookie: krisid_session=<token>; HttpOnly; SameSite=Lax; Path=/;
            Max-Age=43200[; Secure]

Secure is added only when request.url.scheme == "https", because the product runs on http://localhost on a laptop and a Secure cookie there is a cookie the browser drops.

DELETE /api/session sets revoked_at = now on the row and clears the cookie with delete_cookie(SESSION_COOKIE, path="/").

What a 401 looks like

The middleware (session_middleware) reads the cookie, resolves the session, and for any path under /api/ that is not open returns:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"detail": "Your session has ended. Sign in again to carry on."}

_actor raises the same sentence as a 401 if it is somehow reached without a session.

Sign-in failure is a different 401 and is deliberately vague about which half was wrong: {"detail": "That username and password do not go together."}. After 8 consecutive failures within 15 minutes it becomes 429 — "Too many attempts. Wait fifteen minutes, or ask for a new invite link.".

One exception, and it is narrow: if the password given matches a sign-up that is still waiting for its email to be confirmed, the answer is a 403 naming that address and telling the person to follow the link. The vagueness exists so that nobody can discover who works here; somebody who has proved the password already knows, so there is nothing left to protect. A wrong password and an unknown address get the vague sentence either way, and the lookup costs one scrypt whichever way it goes so the clock says nothing either. See POST /api/session.

What needs no session

_is_open(path, method) is the whole list, and it is exact paths and exact methods rather than prefixes:

Path Methods Why
/api/session POST, DELETE signing in, signing out
/api/invites/… any (prefix) look at an invite, or accept it
/api/signup GET, POST whether sign-up is open; sending the form
/api/signup/{token} GET, POST the confirmation link (exactly 3 slashes)
/api/lineage/openlineage POST OpenLineage push; carries its own key
/api/brand, /api/brand/logo GET, HEAD the sign-in screen's name and logo
/api/maintenance GET, HEAD what the maintenance page is drawn from
/api/health GET liveness
/api/licence GET whose copy this is, answerable when nothing else is

Note the deliberate care: /api/signup-settings begins with the same eleven characters as /api/signup and is an administrator endpoint, so the check is path == "/api/signup" and not startswith.

The X-Actor header — honoured nowhere

108 endpoints still declare x_actor: str | None = Header(default=None). It is ignored, deliberately and permanently. _actor(cur, name=None) takes the name argument and never looks at it:

name is the X-Actor header that 108 endpoints still declare. It is ignored, deliberately and permanently: the header was how this worked before there was a login, and leaving it live would mean the login was decoration. verify sends one and proves it changes nothing.

Who you are comes from CURRENT — a contextvars.ContextVar the middleware sets from the session cookie . A ContextVar rather than a global, because one process serves many requests and a global would hand one person's session to the next person's request.

There is exactly one other way in, and it is named rather than hidden: the lineage intake key, sent as Authorization: Bearer <key> to POST /api/lineage/openlineage. It identifies one connection, can do one thing (append an event to that connection's inbox), creates no session, is never seen by _actor, and can read nothing at all — including the events it has itself sent.

Viewing as somebody else

POST /api/session/view-as sets viewing_as_id on the session row. While it is set:

  • every read answers as that person — _actor and _whoever return them ;
  • every write is refused by the middleware, before any endpoint sees it, with 403 and {"detail": "You are viewing the repository as <Name>, which shows you what they see and changes nothing. Stop viewing as them to make this change as yourself."} The only exceptions are /api/session/view-as itself and /api/session.

That is enforced in the middleware rather than per endpoint on purpose, so a new endpoint is safe the day it is written.

Maintenance

While maintenance is on , every API path except /api/maintenance, /api/health, /api/licence, /api/brand, /api/brand/logo and /api/session answers:

HTTP/1.1 503 Service Unavailable

{"detail": "<the message, or 'KRISID is being worked on.'>",
 "maintenance": true, "back_at": "<iso|null>", "contact": "<email|null>"}

Whoever holds ACCESS_MANAGE passes straight through, which is how it gets switched off again (_may_pass_maintenance). The client watches for body.maintenance on any refusal and replaces the screen .

Licensing

Checked per request, because the hostname is a property of the request. If the licence names other hosts and on_mismatch is refuse, every /api/ path except /api/licence answers 451 with {"detail": "<why>", "licence": "wrong_host"}. If on_mismatch is read_only, only non-GET/HEAD/OPTIONS methods get the 451.

Conventions

How errors are shaped

Two shapes, because two things refuse.

FastAPI's HTTPException — every deliberate refusal in app.py:

{"detail": "A sentence written to be read."}

The application's own not-ready answer — NotReady, raised when the database is unreachable, empty, or on the wrong schema version:

{"error": "The database is on schema v83; this build expects v91.",
 "remedy": "Run:  bash run.sh upgrade   — it applies what is missing and keeps everything in the repository."}

Status 503. The client reads both keys, in that order (body.error || body.detail), and keeps remedy to show beside the message.

Database refusals are passed through rather than reworded, from every route. The mapping is an exception handler registered for everything psycopg raises, not a block each endpoint has to wrap itself in — it was a block, wrapped at 27 of the 91 routes that write, and a trigger firing from one of the other 64 came back as 500 Internal Server Error with its sentence left on the server. _refusals maps them:

Postgres error HTTP Message
RaiseException (a trigger) 400 the trigger's own first line
CheckViolation 400 the constraint's own first line
UniqueViolation on relation 409 "That link is already there."
UniqueViolation on asset 409 "An asset of that type already has that key. Keys are what a re-load matches on, so two cannot be the same."
UniqueViolation on responsibility 409 "That person already holds that role here."
UniqueViolation on lineage_view 409 "You already have a saved view of that name on the same thing…"
UniqueViolation, other 409 "That already exists."
ForeignKeyViolation on workflow_transition 409 "A workflow step still gives that role a decision to make…"
ForeignKeyViolation on workflow_action 409 "A workflow action still refers to that…"
ForeignKeyViolation on change_request 409 "A change request is still running on that…"
ForeignKeyViolation on responsibility 409 "Somebody still holds that. Remove the assignments first."
ForeignKeyViolation on relation 409 "Assets are still linked by that. Unlink them first."
ForeignKeyViolation, other 409 "Something else still refers to that, so it cannot be removed while it does."

Two writers meeting is not an outage. DeadlockDetected, SerializationFailure, QueryCanceled and LockNotAvailable are all psycopg.OperationalError, which is also what an unreachable database raises — so a collision used to be answered with "Cannot reach Postgres: deadlock detected. Is it running? Try: brew services start postgresql@16", wrong about the cause and about the cure. They are separate branches now, above the one about the connection:

Postgres error HTTP Message
DeadlockDetected, SerializationFailure 409 "Somebody else was changing the same things at the same moment, so nothing was written. Try that again."
QueryCanceled, LockNotAvailable 503 "That took longer than this is allowed to wait, so nothing was written. Try a smaller change, or again in a moment."

Both mean nothing was written — not part of it — so a client may retry the same request as it stands. Each is recorded in the System log with its Postgres error class by name, so the four are distinguishable without parsing prose.

A bug in this build answers 500, and says which bug

An unhandled exception used to answer with FastAPI's four default words and put its traceback on the server's terminal. It now carries the exception's class and a reference:

{"detail": "Something in this build went wrong handling that request
            (KeyError), so nothing was written. Reference 3ce9a093 — the
            System tab of the Logs screen has it, with the full trace in the
            application log.",
 "reference": "3ce9a093",
 "fault": "KeyError"}

The same reference appears on a trouble line in the System log, carrying the exception's message, and above the traceback in the application log. The message is deliberately not in the answer: a message can carry data, a class name cannot. nothing was written is a fact rather than a reassurance — every write is inside db(), whose connection rolls back when an exception leaves it.

Refusals name the permission and the roles that carry it

Every permission check goes through _may(cur, who, perm, asset_id, doing, what=None). It asks the database function may(username, permission, asset_id), and on a no it asks roles_granting(permission) and builds the sentence:

403 {"detail": "Rohan Iyer may not edit \"CUSTOMERS\". That needs ASSET_EDIT.
                It comes with Steward, Metadata Manager or Sysadmin.
                A role held over a data domain reaches everything the domain
                owns, so the fix is usually one assignment further up, not one
                per asset."}

The shape is: {display_name} may not {doing}[ "{what}"]. That needs {PERM}. then " It comes with A, B or C." (or " It comes with A.", or nothing if no role carries it), then always the sentence about domains.

_may is skipped entirely while a workflow is applying something it approved (_AUTHORITY.on). Database and metamodel refusals still fire — this skips who may ask, never what may be true.

_may_bool(cur, who, perm) answers the same question without the refusal, for a screen deciding what to offer. A hidden button is not a permission check: the endpoint always asks again.

Two endpoints use a different gate:

  • _admin(cur, doing) is _may(..., "ACCESS_MANAGE", None, doing).
  • _logs_or_refuse(cur) allows either LOG_VIEW or being named on the Logs screen, and says so in the refusal.

The per-request SQL breakdown is the one place that asymmetry matters: it asks for LOG_VIEW and nothing else. Being named as a reader of the logs lets somebody see what this installation did; it does not let them read the shape of the build.

What a read needs

Three rules apply to every endpoint at once, in the middleware, rather than to a hundred of them one at a time: no session no API, a session that is viewing as somebody may not write, and — since v91 — what a read needs.

Path Needs
/api/assets/{uid}/history HISTORY_VIEW
/api/metamodel… METAMODEL_VIEW
/api/workflows…, /api/requests…, /api/workflow-controls, /api/workflow-scope… WORKFLOW_VIEW
the catalogue and every way of reading it — /api/assets, /api/table, /api/tree, /api/layer-tree, /api/rollup, /api/search, /api/find, /api/facets, /api/hierarchy, /api/graph, /api/explore, /api/lineage, /api/lenses, /api/views, /api/candidates, /api/stats, /api/semantic, /api/bulk, /api/ask, /api/may, /api/types, /api/change-sets, /api/gone-plan, and the workbooks they come out as ASSET_VIEW

Asked with may_anywhere(username, permission) and not may(username, permission, NULL): reading is all or nothing here, and most people's roles sit on a data domain rather than on the repository. Holding one of these over anything at all counts everywhere; a catalogue that quietly omitted the rows somebody may not see would be a worse answer than a refusal.

The refusal is shaped like every other one:

403 {"detail": "Holds Nothing may not read this. That needs ASSET_VIEW.
                It comes with Viewer, Data Steward or Sysadmin.
                A role held over anything at all carries it: reading is not
                restricted asset by asset."}

Everything else is either gated inside the endpoint, because the endpoint knows which of several answers it is giving — SOURCE_VIEW opens one source while the list of sources is everybody's — or open to anybody signed in, each for a stated reason. app.py holds all three lists (READ_NEEDS, GATED_INSIDE, OPEN_READS), and verify asserts that every GET under /api is in exactly one of them: an endpoint written tomorrow fails the check until somebody decides which it is.

What stays open, and why:

  • The access model. Who exists, what the roles are and who holds them is how somebody finds who to ask. Usernames and email addresses need PERSON_IDENTIFY; the access workbook — the whole model in one file, and the shape an upload is read from — needs ACCESS_VIEW as well.
  • The list of sources, and where this repository can call out to.
  • Your own session, details and notifications; the manual; the brand, the licence and the maintenance state, which have to answer before anybody is anybody.

What a uid is

A uid is a UUID — the asset's public identity. _resolve(cur, uid) is the only place the public uuid and the internal bigint meet:

if not UUID_RE.match(str(uid)):
    raise HTTPException(404, f"'{uid}' is not an asset id")
cur.execute("SELECT id FROM asset WHERE uid = %s", (uid,))
row = cur.fetchone
if not row:
    raise HTTPException(404, "no asset with that id")

UUID_RE is the strict 8-4-4-4-12 hex form, case-insensitive.

{uid} does NOT accept an external key. _resolve matches asset.uid only, and a non-UUID string is a 404 with "'<x>' is not an asset id" rather than a 500. This is stated here because it is a reasonable thing to assume and it is not true: external keys are matched only inside the bulk loader (bulk_plan) and the SQL-lineage resolver (_find_asset), never by a path parameter.

Other {...} path parameters are not asset uids:

Parameter What it is
{uid} on /api/users, /api/groups, /api/roles that row's uid column (a uuid)
{ruid} a responsibility.uid
{uid} on /api/views, /api/lineage-views, /api/workflows, /api/requests, /api/change-sets, /api/runs/... that row's own uid
{code} on /api/sources, /api/connections a connection code, upper-cased by the handler
{code} on /api/types/{code}/relatable an asset type code
{code} on /api/assets/{uid}/attributes/{code} an attribute type code, upper-cased
{kind} on /api/metamodel/{kind} one of type, attribute, attachment, relation
{kind} on /api/connectors/{kind}/fragment a connector kind (snowflake, powerbi, …)
{relation_id} an integer relation.id
{n} on a change set item a zero-based index into the draft's items
{token} an invite or sign-up token
{save_id} an integer semantic_source.id
{transition_uid} a workflow_transition.uid
{username} an app_user.username, lower-cased by the handler
{ref}/{code} on a semantic lock a model ref and a field code

How paging is signalled

There are three shapes, and which one an endpoint uses is stated per endpoint.

has_more + next_offset — /api/logs, /api/assets/{uid}/history. The handler asks for limit + 1 rows, sets has_more = len(rows) > limit, trims to limit, and returns next_offset = offset + limit.

more — /api/access/inheritance. more = len(rows) == limit, which can be a false positive on an exact multiple; the handler says so by construction rather than paying for a count.

paged + after + limit + line_counts + lines_total — /api/runs/harvest/{uid}?full=1.

Several endpoints cap instead of paging, and say so:

Field Meaning
capped: true the count stopped at a ceiling; the number is a floor
ceiling that ceiling (COUNT_CEILING = 10000)
truncated: true something sits at the hop limit, so there is more further out
cut: true the walk itself filled up before the hop limit — the hop picker cannot undo it
cap GRAPH_ASSETS = 1200
inheritance_capped the inheritance badge stopped at INHERITANCE_CEILING = 500
shown / total how many came back against how many there are

Asking where a request's seconds went

Any /api/ request may ask the application to time its own statements, and the answer comes back on a header. It is how a slow screen says what is slow about it, and it is what run.sh timing asks for automatically on any screen it has just measured above half a second.

Send x-krisid-sql: 1. Get back the same header, holding JSON:

Field Meaning
statements how many statements the request ran
ms the total milliseconds spent in the database
slowest up to 25 of them, slowest first — each with ms, rows and sql
plans empty unless plans were asked for (below)

The SQL is trimmed to 140 characters a statement. If statements is larger than the length of slowest, the total not adding up is itself the signal: a statement run many times shows up as the gap rather than as a line.

Send x-krisid-sql: plan and any statement over 250 milliseconds also comes back with its EXPLAIN (ANALYZE, BUFFERS) — real row counts, not estimates. At most three plans in one answer, 2200 characters each; the whole plan also goes to the application log. plan:40 sets the threshold in milliseconds instead, which is how you ask for the plan of everything on a page without a different build.

Four things worth knowing before building anything on it:

  • It needs LOG_VIEW — the permission that opens the Logs screen. Note that this is the permission ONLY: unlike the Logs screen itself, being named as a reader there does not buy it, because this is about the shape of the build rather than about the contents of a log.
  • A refusal does not change the answer. Asking without the permission serves the request exactly as it would have been served and adds x-krisid-sql-refused: LOG_VIEW (or sign in first with no session). A client that saw only a missing header could not tell "you may not" from "this build is older than the feature".
  • It carries no data from the repository. Parameters travel separately from statements, so what is recorded is the SQL this build contains and never a value out of your data. That is also why the header is always ASCII.
  • Nothing is timed unless asked. A request that does not send the header pays one context-variable read per statement, and gets no header back.

A statement that FAILED is reported too, with the time it took before it raised — a query that runs for four seconds and then errors is the one most worth seeing.

Only reads are ever explained: EXPLAIN ANALYZE runs the statement, so a write explained would be a write done twice. The rule is deliberately unclever — the statement must begin with SELECT or WITH and must not contain the words INSERT, UPDATE, DELETE, MERGE or TRUNCATE anywhere — because WITH gone AS (DELETE … RETURNING id) SELECT * FROM gone is a SELECT by its first word.

Anything else consistent across the surface

  • Everything is JSON except the file downloads, which answer application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (xlsx), text/csv; charset=utf-8 (CSV, always with a UTF-8 BOM so Excel does not read it as Latin-1), application/json with a Content-Disposition (workflow export), or an image media type (the brand logo).
  • Writes take a JSON body and the client always sends content-type: application/json. File uploads take multipart/form-data with a field called file.
  • One field per call. Almost every edit endpoint changes one thing, because each save is its own line in History naming the field and both values. The exceptions are the deliberately whole-list writes below.
  • Whole lists, not deltas. PUT /api/logs/viewers, PUT /api/working-days, PUT /api/usage/networks, PUT /api/connectors/availability, PUT /api/lineage-views/{uid} (shares) and PUT /api/workflows/{uid} (the canvas) each replace their whole list. The reason is always the same: what somebody decides is "this is the shape of it", and saving it a row at a time leaves a half-applied policy behind whenever something in the middle is refused.
  • 202 Accepted means a write became a proposal rather than a change. Body: {"proposed": true, "request": "<uid>", "detail": "…"}.
  • 204 is never returned; the client handles it anyway.
  • Workbooks are download → preview → apply. GET …/export hands you the current state, POST …/preview says what a file would do and writes nothing, POST …/apply recomputes the plan against live state and commits only if safe. Three screens share one client panel (uploadPanel) pointed at /api/metamodel, /api/bulk and /api/access.
  • A plan's shape, returned by every preview and apply (p.as_dict): {"safe": bool, "empty": bool, "blockers": [str], "warnings": [str], "added": [{"key","kind","detail"}], "changed": [{"key","field","was","now"}], "removed": [{"key","kind","blocking","dependents"}]} plus filename, and applied on an apply.
  • Derived answers are cached on the change log. _derived(cur, key, build, ttl, stamped) rebuilds once per repository change rather than once per request, with exactly one caller building and the rest waiting. Applies to /api/metamodel, /api/stats, /api/tree, /api/semantic/model, /api/semantic/yaml, /api/ask/home and /api/search/status.
  • A field somebody may not read is ABSENT, not null. PERSON_IDENTIFY takes username and email out of GET /api/access and GET /api/users/{uid} for everybody but the reader themselves, and it takes the KEYS out rather than emptying them: on that screen an empty email already means "nobody has recorded one" next to an editable field and an Invite button, so a null would have been a statement, and a false one. Both payloads also carry a boolean (may_identify, identity_shown) so a client can tell not served to you from not recorded. Where redacting a payload would make it dangerous rather than merely poorer — the access workbook, which is keyed on the usernames — the endpoint refuses with 403 instead.
  • Proposed assets are invisible. asset.proposed_by IS NOT NULL means an asset exists only for whoever proposed it, whoever may decide on it, and whoever manages access (_may_see_proposal). Every list query carries a.proposed_by IS NULL; GET /api/assets/{uid} answers 404 "no such asset" to anybody else, which is true for them.

The permission codes used below

ACCESS_MANAGE, ASSET_BULK_LOAD, ASSET_CREATE, ASSET_DELETE, ASSET_EDIT, ASSET_VIEW, CONNECTION_MANAGE, CONNECTOR_ALLOW, HARVEST_RUN, HARVESTED_DELETE, LOG_VIEW, METAMODEL_MANAGE, METAMODEL_VIEW, PERSON_IDENTIFY, RELATION_MANAGE, RESPONSIBILITY_ASSIGN, SEMANTIC_EDIT, SOURCE_VIEW, TAG_PUSH, USAGE_MANAGE, USAGE_VIEW, VIEW_AS, VIEW_CREATE, VIEW_SHARE, WORKFLOW_BYPASS_LOAD, WORKFLOW_DESIGN, WORKFLOW_RAISE, WORKFLOW_VIEW.

Which roles carry which is data, not code — it is the access workbook — so the refusal reads it at the moment of refusing.

LOG_VIEW buys two things rather than one: the Logs screen, and the right to ask a request where its seconds went (above). PERSON_IDENTIFY buys two as well: other people's username and email address on People and Persona, and the access workbook — which since v91 needs ACCESS_VIEW beside it.

Each permission also carries how far a holding of it reaches, which the People and Persona grid shows beside its name and GET /api/access returns as scope:

scope What it means
asset a role held on a data domain carries it to everything the domain owns
repository only an assignment over the whole repository grants it; putting the role on one asset does nothing with it
anywhere held over anything at all, it counts everywhere — the four view permissions and ACCESS_VIEW

verify asserts the column against how the application actually asks, so a label cannot drift away from the behaviour.

Index: all 210 endpoints

The lookup table. "Screen" is the screen that calls it; "—" means no screen does. "Permission" is what the handler actually checks — session means a live session and nothing more, open means no session at all.

Method Path Screen Permission
POST /api/session Sign in open
DELETE /api/session The frame (profile menu) open
PUT /api/session/zone The frame (on load) session
GET /api/me The frame (on load) session
GET /api/me/details The frame (profile panel) session
PUT /api/me/password The frame (password form) session + the old password
POST /api/session/view-as The frame (view-as) VIEW_AS (+ ACCESS_MANAGE if switched off)
PUT /api/settings/view-as Access → People ACCESS_MANAGE
GET /api/licence The frame (on load) open
GET /api/health The frame (on load) open
GET /api/maintenance Sign in / the frame open
PUT /api/maintenance Access → People ACCESS_MANAGE
GET /api/brand Sign in / the frame open (GET only)
GET /api/brand/logo Sign in / the frame open (GET only)
PUT /api/brand Appearance ACCESS_MANAGE
POST /api/brand/logo Appearance ACCESS_MANAGE
DELETE /api/brand/logo Appearance ACCESS_MANAGE
GET /api/people The frame (on load), Access, one asset session
GET /api/stats The frame (on load), Overview session
GET /api/metamodel The frame (on load), Metamodel session
GET /api/signup Sign up open
POST /api/signup Sign up open
GET /api/signup/{token} Sign up (confirm) open
POST /api/signup/{token} Sign up (confirm) open
GET /api/signup-settings Access → People ACCESS_MANAGE
PUT /api/signup-settings Access → People ACCESS_MANAGE
POST /api/invites Access → People ACCESS_MANAGE
GET /api/invites/{token} Invite open
POST /api/invites/{token}/accept Invite open
GET /api/ask/home Ask, Appearance session
PUT /api/ask/examples Appearance ACCESS_MANAGE
GET /api/search Ask session
GET /api/find Ask (with conditions) session
GET /api/facets Ask, Workflow designer session
GET /api/views Ask (saved searches), type list session
GET /api/lenses Lenses (sidebar), Administration → Manage lenses session
GET /api/lenses/columns Manage lenses (the builder) session
POST /api/lenses Manage lenses (new) LENS_MANAGE
GET /api/lenses/{uid} A lens session; shared with you
PUT /api/lenses/{uid} Manage lenses (save) its creator, a named editor (person or team), or LENS_MANAGE
DELETE /api/lenses/{uid} Manage lenses (delete) its creator, a named editor (person or team), or LENS_MANAGE
GET /api/lenses/{uid}/rows A lens's rows session; shared with you
GET /api/lenses/{uid}/export A lens (Export) session; shared with you
POST /api/views Ask (save), type list (save) VIEW_CREATE
PUT /api/views/{uid} Ask, type list creator, else METAMODEL_MANAGE
DELETE /api/views/{uid} Ask, type list creator, else METAMODEL_MANAGE
POST /api/views/generated Roll-up (count button) session
GET /api/rollup/{type_code} Roll-up session
GET /api/rollup/{type_code}/{uid} Roll-up detail session
GET /api/tree Estate session
GET /api/tree/{uid} Estate (lazy) session
GET /api/layer-tree Layer session
GET /api/layer-tree/{uid} Layer (lazy) session
GET /api/assets Type list session
GET /api/table Type list (with columns) session
GET /api/table/export Type list (download) session
GET /api/hierarchy Type list (hierarchy) session
GET /api/hierarchy/export Type list (download) session
GET /api/gone-plan Type list (clear gone) session
POST /api/clear-gone Type list (clear gone) ASSET_DELETE
GET /api/assets/{uid} One asset, Ask best-match session (+ proposal rule)
GET /api/may/{uid} One asset session
PATCH /api/assets/{uid} One asset ASSET_EDIT
PUT /api/assets/{uid}/attributes/{code} One asset ASSET_EDIT
GET /api/assets/{uid}/history One asset (History) session
GET /api/assets/{uid}/relations One asset (one link group, next page) session
GET /api/assets/{uid}/relatable One asset (link adder) session
GET /api/types/{code}/relatable New asset form session
GET /api/candidates One asset (asset picker) session
POST /api/assets/{uid}/relations/bulk One asset (link adder) RELATION_MANAGE
DELETE /api/assets/{uid}/relations/{relation_id} One asset (unlink) RELATION_MANAGE
POST /api/assets/{uid}/responsibilities One asset RESPONSIBILITY_ASSIGN
DELETE /api/assets/{uid}/responsibilities/{ruid} One asset RESPONSIBILITY_ASSIGN
POST /api/assets New asset form ASSET_CREATE
GET /api/assets/{uid}/reach One asset (a source role) session
GET /api/assets/{uid}/delete-plan One asset (delete panel) session
DELETE /api/assets/{uid} One asset (delete panel) ASSET_DELETE (+ HARVESTED_DELETE)
GET /api/assets/{uid}/gone-plan One asset (clear gone) session
POST /api/assets/{uid}/clear-gone One asset (clear gone) ASSET_DELETE
GET /api/workflow/governing/{type_code} One asset session
GET /api/change-sets One asset, Requests session
POST /api/change-sets/items One asset WORKFLOW_RAISE
DELETE /api/change-sets/{uid}/items/{n} One asset creator only
DELETE /api/change-sets/{uid} One asset creator only
POST /api/change-sets/{uid}/submit One asset creator only
POST /api/lineage/parse One asset (SQL panel) session
POST /api/lineage/accept One asset (SQL panel) RELATION_MANAGE
GET /api/lineage/{uid} Lineage session
GET /api/graph/{uid} Lineage (connections mode) session
GET /api/lineage/{uid}/export Lineage (download) session
GET /api/explore/{uid} Explorer session
GET /api/lineage-views Lineage session
GET /api/lineage-views/{uid} Lineage (a link) can_see_view
POST /api/lineage-views Lineage VIEW_CREATE (+ VIEW_SHARE to share)
PUT /api/lineage-views/{uid} Lineage owner, else METAMODEL_MANAGE; shares are owner-only
DELETE /api/lineage-views/{uid} Lineage owner, else METAMODEL_MANAGE
GET /api/metamodel/admin Metamodel session
GET /api/metamodel/vocabulary Metamodel session
GET /api/metamodel/history Metamodel → History session
GET /api/metamodel/export Metamodel (download) session
POST /api/metamodel/preview Metamodel (upload) session
POST /api/metamodel/apply Metamodel (upload) METAMODEL_MANAGE
POST /api/metamodel/{kind} Metamodel (add/edit a row) METAMODEL_MANAGE
DELETE /api/metamodel/{kind} Metamodel (remove a row) METAMODEL_MANAGE
POST /api/metamodel/{kind}/check Metamodel (as you type) session
POST /api/connectors/{kind}/fragment A source METAMODEL_MANAGE
GET /api/access Access, one asset session; PERSON_IDENTIFY for other people's username and email
GET /api/access/assignments Access → Assignments session; PERSON_IDENTIFY for other people's username
GET /api/access/inheritance Access → Inheritance session
GET /api/access/history Access → History session
GET /api/access/export Access (download) PERSON_IDENTIFY
POST /api/access/preview Access (upload) session
POST /api/access/apply Access (upload) ACCESS_MANAGE
POST /api/users Access → People ACCESS_MANAGE
PATCH /api/users/{uid} Access → People ACCESS_MANAGE
DELETE /api/users/{uid} Access → People ACCESS_MANAGE
GET /api/users/{uid} A person session; PERSON_IDENTIFY for somebody else's username and email
POST /api/groups Access → Teams ACCESS_MANAGE
PATCH /api/groups/{uid} Access → Teams ACCESS_MANAGE
DELETE /api/groups/{uid} Access → Teams ACCESS_MANAGE
GET /api/groups/{uid} A team session
POST /api/groups/{uid}/members A team ACCESS_MANAGE
DELETE /api/groups/{uid}/members/{username} A team ACCESS_MANAGE
POST /api/roles Access → Roles ACCESS_MANAGE
PATCH /api/roles/{uid} Access → Roles ACCESS_MANAGE
DELETE /api/roles/{uid} Access → Roles ACCESS_MANAGE
PUT /api/roles/{uid}/permissions Access → Permissions ACCESS_MANAGE
PUT /api/roles/{uid}/levels Access → Roles ACCESS_MANAGE
POST /api/responsibilities Access → Assignments RESPONSIBILITY_ASSIGN or ACCESS_MANAGE
DELETE /api/responsibilities/{ruid} Access → Assignments RESPONSIBILITY_ASSIGN or ACCESS_MANAGE
PUT /api/assets/{uid}/inheritance Access → Inheritance ACCESS_MANAGE
PUT /api/settings/inheritance Access → Inheritance ACCESS_MANAGE
GET /api/workflow-controls Access → People session
PUT /api/workflow-controls Access → People ACCESS_MANAGE
GET /api/workflows Workflows session
GET /api/workflows/templates Workflows (new) session
GET /api/workflows/palette Workflows (new), designer session
POST /api/workflows Workflows (new) WORKFLOW_DESIGN
GET /api/workflows/{uid} Workflow designer session
PUT /api/workflows/{uid} Workflow designer (save) WORKFLOW_DESIGN
PATCH /api/workflows/{uid} Workflow designer (rename) WORKFLOW_DESIGN
POST /api/workflows/{uid}/publish Workflow designer WORKFLOW_DESIGN
POST /api/workflows/{uid}/retire Workflow designer WORKFLOW_DESIGN
POST /api/workflows/{uid}/copy Workflow designer WORKFLOW_DESIGN
POST /api/workflows/{uid}/try Workflow designer WORKFLOW_VIEW
GET /api/workflows/{uid}/export Workflow designer session
POST /api/workflows/import/preview Workflows (import) WORKFLOW_DESIGN
POST /api/workflows/import Workflows (import) WORKFLOW_DESIGN
GET /api/workflow-scope/preview Workflow designer (scope) session
GET /api/working-days Workflow designer session
PUT /api/working-days Workflow designer WORKFLOW_DESIGN
GET /api/requests Requests, one asset session
GET /api/requests/{uid} A change request session
POST /api/requests/{uid}/take/{transition_uid} A change request the transition's role/permission
GET /api/notifications Requests session
POST /api/notifications/seen Requests session
POST /api/runs/{uid}/retry A change request raiser, else WORKFLOW_DESIGN
GET /api/connections Connections, workflow designer session (trimmed without ACCESS_MANAGE)
POST /api/connections Connections ACCESS_MANAGE
POST /api/connections/{code}/test Connections ACCESS_MANAGE
PUT /api/connections/{code} Connections ACCESS_MANAGE
DELETE /api/connections/{code} Connections ACCESS_MANAGE
GET /api/connectors Sources session
GET /api/connectors/availability Connectors CONNECTOR_ALLOW
PUT /api/connectors/availability Connectors CONNECTOR_ALLOW
GET /api/sources Sources session (trimmed without SOURCE_VIEW)
GET /api/sources/{code} A source SOURCE_VIEW
GET /api/sources/{code}/runs A source (Runs) SOURCE_VIEW
POST /api/sources Sources (new), a source CONNECTION_MANAGE
POST /api/sources/{code}/test A source SOURCE_VIEW + HARVEST_RUN
POST /api/sources/{code}/preview A source SOURCE_VIEW + HARVEST_RUN
POST /api/sources/{code}/run A source SOURCE_VIEW + HARVEST_RUN
POST /api/sources/{code}/intake-key A source CONNECTION_MANAGE
GET /api/sources/{code}/inbox A source SOURCE_VIEW + HARVEST_RUN
GET /api/sources/{code}/tagmap A source (push card) SOURCE_VIEW + METAMODEL_VIEW
POST /api/sources/{code}/tagmap A source (push card) CONNECTION_MANAGE
DELETE /api/sources/{code}/tagmap/{uid} A source (push card) CONNECTION_MANAGE
POST /api/sources/{code}/tagmap/{uid}/approve A source (push card) TAG_PUSH
POST /api/sources/{code}/push/preview A source (push card) SOURCE_VIEW + HARVEST_RUN
POST /api/sources/{code}/push A source (push card) SOURCE_VIEW + TAG_PUSH
GET /api/runs/harvest/{uid} A source, a harvest run SOURCE_VIEW
POST /api/runs/harvest/{uid}/stop A source, a harvest run SOURCE_VIEW + HARVEST_RUN
GET /api/runs/harvest/{uid}/lines.csv A harvest run (download) SOURCE_VIEW
GET /api/gone-settings Sources session
PUT /api/gone-settings Sources ACCESS_MANAGE
GET /api/semantic/yaml Semantic model session
POST /api/semantic/yaml/plan Semantic model session
POST /api/semantic/yaml Semantic model SEMANTIC_EDIT
DELETE /api/semantic/lock/{ref}/{code} Semantic model SEMANTIC_EDIT
GET /api/bulk Load from Excel, Metamodel session
GET /api/bulk/export Load from Excel, type list session
POST /api/bulk/preview Load from Excel session
POST /api/bulk/apply Load from Excel ASSET_BULK_LOAD
GET /api/mail Email ACCESS_MANAGE
PUT /api/mail Email ACCESS_MANAGE
POST /api/mail/test Email ACCESS_MANAGE
GET /api/logs Logs LOG_VIEW or named
GET /api/logs/settings Logs LOG_VIEW or named
PUT /api/logs/viewers Logs ACCESS_MANAGE
GET /api/usage/state The frame (on load), Usage session
PUT /api/usage/settings Usage USAGE_MANAGE
DELETE /api/usage Usage USAGE_MANAGE
POST /api/usage/event The frame (every navigation) session
GET /api/usage/summary Usage USAGE_VIEW
GET /api/usage/networks Usage USAGE_VIEW
PUT /api/usage/networks Usage USAGE_MANAGE
GET /api/usage/export Usage (download) USAGE_VIEW
GET /api/help Help centre session
POST /api/assets/{uid}/relations — RELATION_MANAGE
POST /api/requests — WORKFLOW_RAISE
PUT /api/logs/keep — ACCESS_MANAGE
DELETE /api/workflows/{uid} — WORKFLOW_DESIGN
DELETE /api/sources/{code} — CONNECTION_MANAGE
POST /api/lineage/openlineage — a lineage intake key
GET /api/semantic/model — session
POST /api/semantic/compile — session
GET /api/semantic/history — session
GET /api/semantic/history/{save_id} — session
POST /api/search/rebuild — METAMODEL_MANAGE
GET /api/search/status — session
GET / — (serves the page) open

There is no /api/overview: the Overview screen is drawn from /api/stats.

Every screen's calls

Each page below is one screen of KRISID and every endpoint it calls, in the order it calls them.