KQL (Kirby Query Language)

Extends the official KQL plugin – swaps basic auth for bearer tokens, caches queries, and runs them in the language the request names.

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:

config.php
return [
    'headless' => [
        'token' => 'your-secret-token'
    ],
    'kql' => [
        'auth' => 'bearer'
    ]
];
See the Authentication page for detailed setup instructions.

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:

config.php
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.

Cached responses live in Kirby's pages cache, which Kirby flushes after every content change made through the Panel or the API – only edits written straight to disk leave a stale entry behind.