KQL (Kirby Query Language)
Kirby Headless provides an enhanced KQL endpoint at /api/kql with bearer token authentication, automatic caching, and multi-language support. This extends the official KQL plugin with features commonly needed in headless setups.
Configuration
Enable bearer token authentication in your config.php:
return [
'headless' => [
'token' => 'your-secret-token'
],
'kql' => [
'auth' => 'bearer'
]
];
Making Requests
Send KQL queries to /api/kql with the bearer token in the Authorization header:
const response = await fetch("https://example.com/api/kql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
},
body: JSON.stringify({
query: "site",
select: {
title: true,
children: {
query: "site.children",
select: ["title", "url"],
},
},
}),
});
const data = await response.json();
Multi-Language Support
For multi-language sites, set the language using the X-Language header or a ?language= query parameter. Kirby's API reads the query parameter first, so it wins where a request carries both:
const response = await fetch("https://example.com/api/kql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
"X-Language": "de",
},
body: JSON.stringify({
query: "page('home')",
select: {
title: true,
},
}),
});
Cache Control
Query responses go through Kirby's pages cache, which is inactive until you enable it – without cache.pages, Kirby hands out a dummy cache and every query is answered from scratch:
return [
'cache' => [
'pages' => [
'active' => true
]
]
];
Once the cache is on, disable it per request with the X-Cacheable header:
const response = await fetch("https://example.com/api/kql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.KIRBY_API_TOKEN}`,
"X-Cacheable": "false",
},
body: JSON.stringify({
query: "site",
select: { title: true },
}),
});
The cache key hashes the query and carries the language the request resolved to, so the same query answers each language from its own entry. Pick one way to name the language and keep to it: ?language=de is part of the hashed query, X-Language: de is not, so a frontend sending both conventions caches the same answer twice.