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, so that plugin has to be installed as well: composer require getkirby/kql.

Configuration

Enable bearer token authentication in your config.php:

site/config/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 are cached only once Kirby's pages cache is enabled:

site/config/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 },
  }),
});

Each language is cached separately. Pick one way to name the language and keep to it: the same query sent once with ?language=de and once with X-Language: de is cached 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.