# The /api/ client

Source: https://capacms.com/docs/sdk/client

createClient from @capacms/sdk/next: entries, typed reads, the flat shape, inflate and errors.

Create one client per key and pin the platform version in code:

```ts
import { createClient, CapaError } from "@capacms/sdk/next";

const capa = createClient({
  baseUrl: process.env.CAPA_API_URL!,
  apiKey: process.env.CAPA_KEY!,           // cap_live_..., cap_test_..., or the legacy key your site has
  version: "2026-10-01",
  contract: 1,
});
```

The `/api/` client sends `x-api-key`, `Capa-Version`, optional
`Capa-Contract`, and `Accept: application/json`. It never sends
`X-Tenant-Key`; the tenant comes from the key.

## Entries

```ts
const page = await capa.entries.list("articles", {
  select: [
    "title",
    "views",
    { author: ["name"] },
    { coauthors: { select: ["name"], limit: 5, sort: "-name" } },
  ],
  filter: {
    views: { gte: 10 },
    "author.name": { eq: "Ada Vale" },
  },
  sort: ["-views"],
  limit: 25,
  count: true,
});

for await (const entry of capa.entries.iterate("articles", { select: ["title"] })) {
  console.log(entry.fields.title);
}

const one = await capa.entries.get("articles", "entry-id", {
  select: ["title", { author: "*" }],
});
```

`select` may be the grammar string the API reference describes
([capacms.com/docs/api/entries](https://capacms.com/docs/api/entries)) or the object form above. Lists return `{ data, page, meta, cacheTags }`; singles return
`{ data, meta, cacheTags }`. `cacheTags` is parsed from the `Surrogate-Key`
header. `get()` returns `null` for `404 entry_not_found` and throws every other
error.

Filters use the `/api/` operators:

```ts
await capa.entries.list("articles", {
  filter: {
    id: { in: ["00000000-0000-4000-8000-000000000021", "00000000-0000-4000-8000-000000000022"] },
    views: { gte: 10 },
    tags: { hasAny: ["news", "launch"] },
  },
  where: { or: [{ featured: { eq: true } }, { views: { gt: 100 } }] },
});
```

Unknown filter operators throw a local `TypeError` before any request is sent.
Per-call `{ signal }` is forwarded to `fetch`.

### Typed reads

`capa-codegen` writes an interface per model (see Codegen). Pass it, and the
select's own type, and `fields` is typed as the API returns it:

```ts
import type { Articles, ArticlesSelect } from "./capa-types"; // written by capa-codegen

const select = [
  "title",
  { author: ["name"] },
  { coauthors: { select: ["name"], limit: 3 } },
] as const satisfies ArticlesSelect;

const typed = await capa.entries.list<Articles, typeof select>("articles", { select });

for (const article of typed.data) {
  const { title, author, coauthors } = article.fields;
  if (author && "fields" in author) console.log(title, author.fields.name);
  for (const coauthor of coauthors.items) {
    if ("fields" in coauthor) console.log(coauthor.fields.name);
  }
  article.fields.body; // compile error: the select does not name it
}
```

* A relation the select expands is the related entry, with its own `fields`,
  or `{ id, model, missing: true }` when that entry was deleted, is
  unpublished for a production key, or is in a model the key cannot read.
  `"fields" in author` tells them apart.
* A relation the select names without expanding it (`"author"`, or every
  relation under `*`) is a reference, `{ id, model }`.
* A relation list is `{ items, pageInfo }`, expanded or not.
* Media is `{ id, url, alt, type, width, height }`.
* A field the select names is always there, and `null` when it was never
  filled, as the API writes it. `title?: string` in the model reads as
  `string | null`.
* A field the select does not name is not there, so reading it does not
  compile.

Without `typeof select`, or with a select written as a string, every field is
typed, and each relation as whichever of the three it may be. `get` and
`iterate` take the same two types, and `EntryFields<Articles, typeof select>`
names the type of `fields`.

## Flat responses: each related entry once

By default an expanded relation is nested where you selected it, so twenty
articles by one author carry that author twenty times. Pass `shape: "flat"` and
every relation comes back as a `{ id, model }` reference, with each expanded
entry once in `included`, keyed by model namespace and then id:

```ts
const select = ["title", { author: ["name"] }] as const satisfies Select<Article>;
const flat = await capa.entries.list<Article, typeof select>("articles", {
  select,
  shape: "flat",
});

flat.data[0].fields.author;             // { id: "…", model: "authors" }
flat.included.authors[authorId].fields; // { name: "Ada Vale" }, typed from Author
```

`included` is typed by the select: the union of the entry types it expands, at
any depth, each field of which may be absent, since an entry holds what every
path that reached it selected. A relation in `data` or in `included` is a
reference. A select written as a plain string types `included` as
`Record<string, unknown>`. `get` takes `shape: "flat"` the same way.
`iterate` reads the tree shape only.

`inflate` turns a flat result back into the tree result, deep-equal to what the
same request without `shape` returns:

```ts
import { inflate } from "@capacms/sdk/next";

const tree = inflate(flat); // { data, page, meta, cacheTags }, typed as the tree read
```

* It walks the select the request sent. A result from this client carries it
  (`flat.select`); for a body you fetched yourself, pass it:
  `inflate(body, "title,author(name)")`.
* `$tags`, `$createdAt` and the other `$` names in the select are the system
  keys, as the API reads them, so `select=$tags,tags` on a model with its own
  `tags` field comes back with both.
* It returns copies. The same author under twenty articles is twenty equal,
  independent objects, as when a tree body is parsed. The input is not changed.
* Cycles end where the select ends: `a` related to `b` related to `a` is
  inflated to the depth you wrote and no further.
* An entry reached by two paths holds the union of what they selected, and one
  value per field. If two paths expand the same array relation with different
  `limit` or `sort`, the first one wins, and `inflate` cannot tell them apart.
  Every other request round-trips exactly.
* In edit mode, included entries are marked, and so is every copy `inflate`
  makes of them.

## Errors

```ts
try {
  await capa.entries.list("articles", { limit: 500 });
} catch (error) {
  if (error instanceof CapaError) {
    console.log(error.status, error.code, error.param, error.hint, error.requestId);
  }
}
```

`CapaError` carries `{ status, type, code, message, param, hint, requestId,
docs }`. A non-JSON response is reported as `code: "unparseable_response"`.
