Keel is a protocol. The Kotlin core and Ktor adapter are a reference implementation. This page is the wire format: what goes on the network, not how Ktor happens to bind it.
Format coordinate: keel/1. Seed version field v defaults to 1.
Seed JSON
Document GETs embed the seed in #__keel_seed. Visits return the same object
as application/json. Fields:
| Field | Required | Meaning |
|---|---|---|
v |
yes (default 1) | Seed version |
page |
yes | Page id |
path |
yes | Absolute URL path, including query |
params |
yes | Path captures |
data |
yes | Page payload object |
errors |
yes | { field: string[] } |
theme |
yes | { id, version } of the serving pack |
entry |
yes | URL of the page module |
css |
yes | Stylesheet URLs |
build |
yes | Serving pack content hash (X-Keel-Build); a change forces a client reload |
host |
yes | Mount selector, default #__keel_root |
shared |
no | Cross-page bag |
layout |
no | Layout module id |
redirect |
no | Absolute path to follow |
head |
no | { title, description, canonical, image, type, html }; html is sanitized pack markup |
Headers
| Header | Direction | Meaning |
|---|---|---|
X-Keel-Visit: true |
request | Return JSON, not the HTML shell |
X-Keel-Only |
request | Comma-separated top-level data keys to keep |
X-Keel-Except |
request | Comma-separated top-level data keys to drop |
X-Keel-Partial |
response | Comma-separated keys actually retained |
X-Keel-Theme |
response | Serving pack id |
X-Keel-Version |
response | Serving pack version |
X-Keel-Build |
response | Serving pack content hash; clients reload when it changes |
Documents and visits both carry X-Keel-Theme, X-Keel-Version, and
X-Keel-Build. Selections never travel in the other direction.
only / except apply only on visits. Document GETs always return the
full seed — they are the SEO unit. The client shallow-merges a partial
data object over the previously mounted page’s data. Partial responses
must not be written to the prefetch cache.
Status codes
| Code | When |
|---|---|
| 200 | Document, visit, or successful action |
| 404 | Unknown page or unknown action. Page 404s still return a seed when a not-found page is configured |
| 422 | Validation. Visits return a seed with errors. Actions return { "errors": { … } } |
| 403 | CSRF rejection. { "errors": { "csrf": ["…"] } } |
| 405 | HTTP method not declared on the page |
Pack schema
GET /__keel/schema is the machine-readable contract. A running host emits
the same keel/1 JSON Typegen writes offline:
{
"format": "keel/1",
"pagesName": "Pages",
"pages": {
"harbor.home": {
"type": "HomePage",
"path": "/",
"methods": [
"GET"
]
}
},
"actions": {
"harbor.setName": {
"in": "SetNameIn",
"out": "SetNameOut"
}
},
"types": {
"HomePage": {
"kind": "object",
"fields": {
"feed": "FeedItem[]"
}
}
}
}
keel-scaffold <origin> <dir> fetches this URL and writes blank pack pages.
See Typegen.
Actions
POST /__keel/action/{id} with a JSON body. Success: { "data": … }.
Validation: 422 { "errors": … }. Unknown id: 404.
Path grammar
Owned by Keel, not inherited from a framework:
{name}— required segment{name?}— optional trailing segment{name...}— tailcard (remainder, possibly empty)- anything else is a literal
Tailcards and optionals must be last. Duplicate names are rejected. Paths
must be absolute. When several patterns match, specificity wins:
literal > required param > optional > tailcard. That is why /p/new beats
/p/{slug} beats /p/{rest...}.
Pages default to GET. POST/PUT/PATCH/DELETE are opt-in on the binding.
/__keel/navigate is a compatibility proxy and honours the same method set.
CSRF
Writes (anything other than GET/HEAD/OPTIONS) are allowed when they carry a
non-CORS-simple marker — Content-Type: application/json or X-Keel-Visit
— and Origin / Sec-Fetch-Site is same-origin or allowlisted. The
policy lives in dev.kolektiv.keel.security so a non-Ktor host can reuse it.
Hosts should set session cookies SameSite=Lax.
See Host adapter for the server checklist and Pack adapter for the client module contract.