HTTP API
konstruct serve answers questions about the graph over HTTP, under /api/v1/, for tools that cannot run Python or speak MCP. The contract is an OpenAPI 3.1 document, served at /api/v1/openapi.json and published here as openapi.json; both are made from the server’s own route table, so they describe exactly what it serves.
konstruct serve --no-build &
curl -s localhost:8000/api/v1/impact/model.jaffle_shop.stg_payments%23amount | jq .summary
The API is read-only. It never writes a file, runs SQL or connects to a warehouse; konstruct verify and konstruct build are not part of it.
The Viewer
/ is the viewer, the same page as graph.html but for one element in its head, <meta name="konstruct-serve" content="">, which makes it load /graph.json and /explain.json from the server rather than from the page. /explain.json holds what graph.html embeds beside the graph: each column’s formula and each relation’s conditions. Both carry strong ETags and are sent with gzip or brotli when the client accepts them.
/events is a stream of server-sent events. An unnamed event arrives on connecting and again each time a build replaces graph.json; its id is the graph’s version and its data is {"version", "nodes", "edges"}. The viewer reloads the graph in place on each one, keeping the page and the view. At most 64 streams are open at once (too-many-streams below).
Endpoints
Every endpoint takes GET (and HEAD) and answers JSON unless noted. Each calls the same Python function as the command line and the MCP server, so the three give one answer. Endpoints with {id}, from and to name nodes by id (percent-encode # as %23); the others take any name the command line takes. An answer is kept until the graph changes, except where it reads git or the project’s files. Python answers one question at a time: a kept answer is sent at once, and the others wait their turn, up to eight at once (busy).
Endpoint |
Answer |
Kept |
|---|---|---|
|
The node as |
yes |
|
Nodes by kind (default: relations), paged |
yes |
|
The nodes a name such as |
yes |
|
Everything downstream, as |
yes |
|
Everything upstream, as |
yes |
|
The shortest connection between two nodes |
yes |
|
Nodes ranked by the words of |
yes |
|
One node in detail, as |
yes |
|
A column’s formula, composed down to its sources |
yes |
|
A business term, or the terms bound to a column |
yes |
|
Every business term, paged |
yes |
|
Entities, measures, relationships and broken rules |
yes |
|
Columns that compute one business idea in different ways, paged |
yes |
|
A question in words, as |
yes |
|
When a column’s formula changed in git |
no |
|
What a change does, column by column |
no |
|
The gate’s findings, for the graph as served ( |
no |
|
What Konstruct finds in the project, and whether the graph is current |
no |
|
The figures REPORT.md summarises |
yes |
|
|
yes |
|
The OpenAPI document |
yes |
Each answer is the to_dict() of the matching result in the Python API, so its fields are documented there.
curl -s 'localhost:8000/api/v1/search?q=lifetime+value&kind=column&limit=5'
curl -s 'localhost:8000/api/v1/path?from=seed.jaffle_shop.raw_payments&to=exposure.jaffle_shop.customer_dashboard'
curl -s localhost:8000/api/v1/explain/customers.customer_lifetime_value | jq .composed
curl -s 'localhost:8000/api/v1/export/mermaid?select=orders.amount&columns=true'
Paging
Lists (nodes, terms, search and conflicts) return one page and a next_cursor, which is null on the last page. Pass it back as cursor with the same parameters for the next page. limit is from 0 to 1000. The order is the same on every page, and a cursor belongs to one version of the graph: after a rebuild it is refused with stale-cursor, and paging starts again.
curl -s 'localhost:8000/api/v1/nodes?kind=column&limit=100' | jq .next_cursor
curl -s 'localhost:8000/api/v1/nodes?kind=column&limit=100&cursor=4f1c2a90b3e8d7a6.100'
Errors
Errors are RFC 9457 problem details, sent as application/problem+json:
{"type": "https://kodexlabsio.github.io/konstruct/reference/http-api.html#no-such-node", "title": "Nothing in the graph has this id or name", "status": 404, "detail": "nothing in the graph matches 'orders.amont'; did you mean orders.amount?"}
The type is one of these, and does not change within /api/v1/:
bad-request
The request is malformed, such as one with two
Hostheaders.
invalid-parameter
A required parameter is missing, or one is not of its type or not one of its values. A
base,headorrefthat starts with-is refused, since git would read it as an option.
host-not-allowed
The
Hostheader does not name the server. Requests must name it by IP address,localhostor the--hostgiven, which stops other sites reaching it through DNS rebinding.
cross-site
A browser sent the request from a page on another site (
Sec-Fetch-Siteiscross-siteorsame-site, orOriginnames another origin). Only the viewer itself, at/, may be opened from a link elsewhere; its files, events and the API are for the viewer and for clients that are not browsers.
not-found
No route has this path.
no-such-node
No node has this id, or nothing matches the name.
method-not-allowed
Only
GETandHEADare served.
stale-cursor
The graph was rebuilt after the cursor was made.
not-answerable
The question does not apply: a name that is ambiguous,
explainof a table,diffoutside a git repository.
internal-error
The answer failed;
detailsays why, with each file named but not where it is. The server’s output has the whole error.
busy
503, with Retry-After: 1. Eight questions are waiting for an answer already. The answers come from the Python API one at a time, so a question that is not kept (above) waits for those before it; a kept answer is sent at once, busy or not. Ask again after a moment.
too-many-streams
64
/eventsstreams are open already, one for each open viewer page; the connection is closed. Close a page, or reload once another has closed.
Access
By default the server listens on 127.0.0.1, and only this machine can connect. With --host set to another address it requires a bearer token on every request: KONSTRUCT_SERVE_TOKEN when it is set, otherwise one made at start-up and printed in the address. The viewer’s address carries it as ?token=, which sets a cookie for the page’s own requests; other clients send it as a header:
KONSTRUCT_SERVE_TOKEN=$(openssl rand -hex 24) konstruct serve --host 0.0.0.0
curl -s -H "Authorization: Bearer $KONSTRUCT_SERVE_TOKEN" http://10.0.0.5:8000/api/v1/status
The server speaks plain HTTP, so the token and every answer cross the network unencrypted. Before using --host beyond a network you trust, put a proxy that terminates TLS in front of the server (Caddy, nginx or a cloud load balancer), keep --host on an address only that proxy can reach, and have the proxy pass the Host the server was started with.
Other machines reach the server by its IP address or by the name given to --host. Every response carries X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and a Content-Security-Policy, and none allows other origins to read it.