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
The session cookie
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:
nameis 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.verifysends 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 —
_actorand_whoeverreturn them ; - every write is refused by the middleware, before any
endpoint sees it, with
403and{"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-asitself 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 eitherLOG_VIEWor 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 — needsACCESS_VIEWas 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(orsign in firstwith 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/jsonwith aContent-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 takemultipart/form-datawith a field calledfile. - 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) andPUT /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 Acceptedmeans a write became a proposal rather than a change. Body:{"proposed": true, "request": "<uid>", "detail": "…"}.204is never returned; the client handles it anyway.- Workbooks are download → preview → apply.
GET …/exporthands you the current state,POST …/previewsays what a file would do and writes nothing,POST …/applyrecomputes the plan against live state and commits only ifsafe. Three screens share one client panel (uploadPanel) pointed at/api/metamodel,/api/bulkand/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"}]}plusfilename, andappliedon 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/homeand/api/search/status. - A field somebody may not read is ABSENT, not null.
PERSON_IDENTIFYtakesusernameandemailout ofGET /api/accessandGET /api/users/{uid}for everybody but the reader themselves, and it takes the KEYS out rather than emptying them: on that screen an emptyemailalready means "nobody has recorded one" next to an editable field and an Invite button, so anullwould 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 with403instead. - Proposed assets are invisible.
asset.proposed_by IS NOT NULLmeans an asset exists only for whoever proposed it, whoever may decide on it, and whoever manages access (_may_see_proposal). Every list query carriesa.proposed_by IS NULL;GET /api/assets/{uid}answers404"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 |
ACCESS_MANAGE |
|
| PUT | /api/mail |
ACCESS_MANAGE |
|
| POST | /api/mail/test |
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.
- Signing in and the frame
- The invite and sign-up screens
- Ask and search
- Browsing: overview, roll-up, estate, layers, type lists
- One asset
- Lineage and the explorer
- The metamodel
- People, teams, roles and access
- Workflows, requests and change sets
- Sources, connections and harvesting
- The semantic layer
- Bulk load and exports
- Administration
- The help centre
- Not called by any screen