Skip to content

API Docs Endpoint

A running Kirak app describes its own HTTP API in two forms:

For Format
GET /docs AI agents (and people) calling the API Markdown, one document
GET /openapi.json API clients, SDK generators, gateways OpenAPI 3.1

Both are built from the same description of the running app, so they never disagree. Both exist only on a running app: they describe the routes and models it has actually loaded.

Swagger UI and ReDoc are not served. Import openapi.json into an API client such as Postman, Insomnia or Bruno for interactive use.


An agent reads it once and can then call any endpoint correctly:

  • Quick Reference – a map of the API first: the models, the CRUD and GraphQL paths, how to authenticate, the enabled modules, where openapi.json is, and the official SDKs and coding-agent integrations (listed in kirak/catalog/specs/resources.py; an entry without a URL is not shown).
  • Not Supported – what agents most often assume exists: REST relationship expansion, a get-by-id route, whole-table writes, GraphQL introspection and subscriptions, a guest fallback for a bad credential.
  • Authentication – how to send a token or API key, what happens without one, when to refresh, how to read an access rule’s condition, and the session flow (register, login, me, refresh, logout) with what each step sends and returns.
  • Responses – the success and error envelope, stated once, and the stable error codes of the core and of each enabled module that lists them (see Response Envelope).
  • CRUD Operations – one table for every model (method, path, input, what data holds), followed by the filter operators, a complete fetch response with its pagination, sorting and rate limits, that REST does not expand relationships, and example requests on one of your models, written for a named role (tests check them against the validator and the query parser).
  • Models – one compact block per model: fields with their flags (read-only, required, unique, searchable, max 200, one of: ..., -> users for relationships), access rules per operation, field-level rules and the model’s own rate limits. read-only fields (id, the timestamps, primary keys) are set by Kirak; values sent for them are ignored; every other field is writable. An ownership rule reads user (own records: user_id = their user_id), and a create rule like that is stated as an owner field: create and upsert fill it from the caller.
  • GraphQL – the query, aggregate and mutation root fields, filter syntax, joins, and that several mutations in one request run in one transaction. GraphQL introspection is disabled, so this section is the schema. The examples are built from your models.
  • Modules – for each enabled module: its summary, the things an agent gets wrong, the configured provider instances (names and types, never secrets) and its endpoints, grouped by their OpenAPI tag when a module uses several (auth: session, password, MFA, …). An endpoint with a JSON body lists its fields (Body:, * marks a required one), from the operation’s parameters in the module catalog (kirak modules <name> --json). Only what the configured providers use is listed: a field only some provider types read appears when one of them is configured, and a route whose operation none of them supports (the /payments/connect/* routes without a stripe_connect instance) lists no fields.
  • MCP servers – with the mcp module on, under Modules: each server’s path, description, access level and tools (from its file and the Python tools registered on it). They are for MCP clients; the Quick Reference names them too.
  • Custom Endpoints – your own routes, grouped by tag.
  • System APIs – last, and named only: admin, monitoring and scheduler serve operators and tooling, not app clients, so their routes are not listed (openapi.json has them).

Models marked "internal": true are left out everywhere, as the CRUD routes return 404 for them and GraphQL rejects them.

The document is built once at startup and cached; models and routes do not change after that.

Two query parameters cut the document down, for a large app or a frontend for one kind of user:

  • GET /docs?role=user – only the models, operations, access rules, owner fields and fields that role can use (the same decision the access engine makes). Module and custom routes stay, since their access is not declared where /docs can read it.
  • GET /docs?models=posts,comments – only those models. An unknown or internal model answers 400 VALIDATION_ERROR.

?role only chooses what the document describes: it grants nothing, and every request is still checked against the caller’s own role. Both combine. Names are letters, digits and _ . * - only; anything else is rejected, so no other text from the URL reaches a document agents read. Views are rendered on first use and cached.

Every response carries an ETag; send it back in If-None-Match to get 304 Not Modified when nothing changed, e.g. to know whether a cached copy is stale.

FastAPI’s own document, with the generic CRUD paths (/{model_name}/fetch, …) replaced by one set per model (/posts/fetch, /posts/create, …), each tagged with its model and carrying:

  • request bodies from the model’s fields (<Model>Create, <Model>Update) and the record shape (<Model>; password fields are left out of it)
  • filter, pagination and sorting parameters
  • security: a bearer token or API key, optional when the operation allows guest
  • the success and error envelopes (KirakSuccess, KirakError) and rate-limit headers

Kirak rules OpenAPI has no field for are in extensions, which standard tools ignore:

Extension Where Content
x-kirak-access each CRUD operation the access rules: roles and row-level conditions
x-kirak-rate-limit each CRUD operation with a limit max_requests, window_seconds, per
x-kirak-filter-operators read operations, /graphql the filter operators
x-kirak-searchable search the searchable fields
x-kirak-soft-delete model tag the model uses soft delete
x-kirak-relationship foreign-key properties the related model
x-kirak-field-access record schema field-level read and write rules
x-kirak-graphql POST /graphql query and mutation root fields per model
x-kirak-operation module routes the module operation the route runs, e.g. payments.initiate_payment
x-kirak-effect module routes read, write or destructive
x-kirak-providers module request body properties the provider types that read the field, when not all do

Auth, admin, module and custom routes appear as FastAPI renders them, except that a module route running a module operation gets that operation’s description and, for a JSON body, its parameters as the request body schema (types, required fields, allowed values); the parameters the route takes from its path or query string, or fills itself, are left out.

In kirak.json (all default to on and public):

{
"docs": { "enabled": true, "path": "/docs", "public": true },
"openapi": { "enabled": true, "public": true }
}
  • docs.enabled: false – no /docs route (404). openapi.json still works.
  • docs.public: false – /docs needs a signed-in caller or an API key (401 otherwise).
  • openapi.enabled: false – no /openapi.json route (404). /docs still works.
  • openapi.public: false – /openapi.json needs a signed-in caller or an API key (401 otherwise). API clients importing it by URL then have to send the token.

The two public settings are separate, so a tool can keep reading one while the other is closed. Both documents describe the same models, fields and access rules: to keep them from anonymous callers, set both docs.public and openapi.public to false.

Use kirak.local.json to differ per environment, for example public in development and closed or off in production.

/docs reads what FastAPI already knows about a route:

@app.get("/reports/monthly", summary="Monthly sales report", tags=["reports"])
async def monthly_report(month: str):
"""Totals per product for one month. month is YYYY-MM."""
...
  • summary= (or the first sentence of the docstring) is the one-line description.
  • The rest of the docstring is shown under it; use it for body fields and rules.
  • tags= groups routes under a heading.
  • openapi_extra={"x-agent-doc": "..."} replaces the text /docs shows for that route, without changing openapi.json.
  • include_in_schema=False hides a route from both.