sdk method reference

createClient and the read methods — list, getBySlug, getById — with locale, errors, and the delivery-token header.

sdk method reference

@chilicms/sdk is the read-side client for the ChiliCMS delivery API. The v0 surface is deliberately small: one factory and three methods. It wraps fetch, unwraps the { data } envelope, and maps a 404 to null. Nothing else.

This page documents the shipped v0 client (packages/sdk-js). Per-content-type typed clients (chili.posts.list(...)) land with codegen in a later phase; until then you pass the content type as a string and supply the row type as a generic.

install

bun add @chilicms/sdk

createClient

import { createClient } from "@chilicms/sdk"
 
const chili = createClient({
  baseUrl: "https://api.chilicms.dev",
})

createClient(opts) returns a ChiliCMSClient. Options:

optiontyperequirednotes
baseUrlstringyesAPI origin, no trailing slash. A trailing slash is stripped for you.
fetchtypeof fetchnoOverride the fetch implementation. Defaults to the global fetch.

The client targets ${baseUrl}/api/v1/<typeSlug>. typeSlug is the URL-plural of your content type — "posts", "pages", or any type you defined in chili.config.ts.

the delivery-token header

Every delivery read is fail-closed: the API resolves a per-tenant delivery token before it opens a row-level-security transaction. A request with no token, a malformed token, or a revoked token gets a 401 — there is no public fallback tenant. Unknown, revoked, and suspended all return the same 401, so the response never tells an attacker which case it hit.

The API reads the token from X-ChiliCMS-Delivery-Token first, then falls back to Authorization: Bearer <token>. The v0 client does not own a token option yet, so attach the header through the fetch override:

import { createClient } from "@chilicms/sdk"
 
const token = process.env.CHILICMS_DELIVERY_TOKEN
 
const chili = createClient({
  baseUrl: "https://api.chilicms.dev",
  fetch: (input, init) =>
    fetch(input, {
      ...init,
      headers: { ...init?.headers, "X-ChiliCMS-Delivery-Token": token },
    }),
})

The delivery token is read-only and tenant-scoped: it only ever returns published content for one tenant. Treat it as a secret — read it from the environment, never inline it in client-shipped code.

list

list<T>(typeSlug: string, opts?: { locale?: string }): Promise<T[]>

Fetch every published entry of a content type. The API returns a { data: [...] } envelope; list unwraps it and hands you the array.

type Post = { id: string; title: string; slug: string; locale: string }
 
const posts = await chili.list<Post>("posts")

Pass opts.locale to scope the read to one locale. The value is validated against the server allowlist — en, de, fr, es, nl, it. An unrecognized locale is a 400, which surfaces as a thrown error (see error handling).

const dutch = await chili.list<Post>("posts", { locale: "nl" })

Omit locale and the request carries no locale query parameter; the API serves every locale for the tenant.

getById

getById<T>(typeSlug: string, id: string): Promise<T | null>

Fetch one entry by its UUID. The id is URL-encoded for you. A 404 returns null rather than throwing, so a missing entry is a value you branch on, not an exception you catch.

const post = await chili.getById<Post>("posts", "9f1c…")
 
if (post === null) {
  // not found — render a 404 page
}

getById does not take a locale option. An entry's id resolves to exactly one row regardless of locale.

getBySlug

getBySlug<T extends { slug: string }>(
  typeSlug: string,
  slug: string,
  opts?: { locale?: string },
): Promise<T | null>

Fetch one entry by its slug. Returns the first match, or null when nothing matches.

const post = await chili.getBySlug<Post>("posts", "hello-world", { locale: "en" })

A slug is unique per (tenant, slug, locale), so the same slug can exist in more than one locale. Pass opts.locale to disambiguate — without it, getBySlug returns whichever locale's entry it finds first.

In v0, getBySlug calls list and filters in the client. It carries the cost of a full list per lookup. For hot paths, prefer list once and index the result yourself; the method routes to a server-side slug index transparently once the API exposes one.

error handling

The client throws on any non-ok HTTP status that is not a handled 404:

callconditionresult
listresponse not okthrows Error — message includes the type slug and status
getByIdstatus 404returns null
getByIdother non-ok statusthrows Error — message includes the slug, id, and status
getBySlugunderlying list not okthrows (propagated from list)
getBySlugno entry matchesreturns null

A thrown error carries the status in its message — for example list posts: 401 when the delivery token is missing or invalid, or list posts: 400 when the locale is off the allowlist. Wrap calls that can fail at the boundary:

try {
  const posts = await chili.list<Post>("posts", { locale: "nl" })
  return posts
} catch (err) {
  // err.message is e.g. "list posts: 401" (token) or "list posts: 400" (locale)
  console.error("delivery read failed:", err)
  return []
}

method reference

methodreturnslocalenot-found
list(typeSlug, opts?)T[]opts.localeempty array
getById(typeSlug, id)T | nullnull
getBySlug(typeSlug, slug, opts?)T | nullopts.localenull

Next: read the concepts behind the delivery model, or self-host the API the client reads from.