# @content-island/api-client

> Content Island - REST API Client

Latest version **0.26.0** (published 2026-08-17) · MIT license · 0 weekly downloads

## Install

```sh
npm install @content-island/api-client
pnpm add @content-island/api-client
yarn add @content-island/api-client
bun add @content-island/api-client
```

Provides the command `content-island`.

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.26.0 |
| Published | 2026-08-17 |
| First published | 2023-09-19 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 0 |
| Unpacked size | 100.9 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Lemoncode |
| Maintainers | content-island |

## Links

- npm: https://www.npmjs.com/package/@content-island/api-client
- npm.io page: https://npm.io/package/@content-island/api-client

## Recent versions

- 0.26.0 (latest) — 2026-08-17
- 0.25.0 — 2026-08-10
- 0.24.1 — 2026-08-05
- 0.24.0 — 2026-07-02
- 0.23.0 — 2026-06-17
- 0.22.0 — 2026-06-04
- 0.21.0 — 2026-06-01
- 0.20.0 — 2026-05-21
- 0.19.0 — 2026-05-18
- 0.18.0 — 2026-03-26
- 0.17.0 — 2026-03-03
- 0.16.0 — 2025-12-16
- 0.15.0 — 2025-11-25
- 0.14.1 — 2025-11-14
- 0.14.0 — 2025-10-31
- … 18 more at https://npm.io/package/@content-island/api-client/versions

## README

# @content-island/api-client

## Installation

```bash
npm install @content-island/api-client
```

## Authentication

The client is configured with a single `accessToken` option that is sent on every
request as `Authorization: Bearer <accessToken>`. Content Island supports two kinds
of tokens:

- **Read token** — grants read-only access to the project. Use it for any `get*`
  method (fetching content, listing, sizing, project metadata).
- **Write token** — grants both read and write access. Required for any method
  that creates, updates, or uploads data.

A write token is a strict superset of a read token: it works for everything. A
read token only works for the read methods. Calling a write method with a read
token returns `403 Forbidden`.

```typescript
import { createClient } from '@content-island/api-client';

// Read-only consumer (e.g. SSG/SSR site fetching content):
const reader = createClient({ accessToken: '<your-read-token>' });

// Authoring tools, ingestion pipelines, admin scripts:
const writer = createClient({ accessToken: '<your-write-token>' });
```

### Token required per method

| Method                    | Token required    |
| ------------------------- | ----------------- |
| `getProject`              | Read **or** Write |
| `getContentList`          | Read **or** Write |
| `getContent`              | Read **or** Write |
| `getRawContentList`       | Read **or** Write |
| `getRawContent`           | Read **or** Write |
| `getContentListSize`      | Read **or** Write |
| `createContent`           | **Write**         |
| `updateContentFieldValue` | **Write**         |
| `uploadMedia`             | **Write**         |

## Examples

### Basic Usage

```typescript
// Your model
interface Post {
  id: string;
  title: string;
  body: string;
  order: number;
  language: 'es' | 'en';
}
```

```typescript
import { createClient } from '@content-island/api-client';

const client = createClient({ accessToken: <your-token>});

const postsWithDefaultLanguage = client.getContentList<Post>({ contentType: 'post'}); // Retrieve the list of contents in the project filtered by content type, for example 'post' in the
const englishPosts = client.getContentList<Post>({ contentType: 'post', language: 'en'}); // Get english posts
const spanishPosts = client.getContentList<Post>({ contentType: 'post', language: 'es'}); // Get spanish posts

// Or you can retrieve a content by id
const postById = client.getContent<Post>({ id: 'content-id', language: 'es' }); // Retrieve a content by id
const postBySomeField = client.getContent<Post>({ 'fields.title': 'post-title', language: 'es' }); // Retrieve a content by field value

```

## Snapshot mode

The client can serve content reads from a **content snapshot** — a single JSON
document exported from your project — instead of hitting the network on every
request. This is the recommended way to consume content from a static site
generator (Astro, Next.js, Gatsby, …): you export the snapshot once at build time
and the build reads from it with zero API round-trips.

There are two modes:

- **`'api'`** (default) — every read is a network request to the B2B API,
  exactly as before.
- **`'snapshot'`** — reads are served from a snapshot file loaded from disk, with
  no network request. The snapshot engine reproduces the live API's filtering,
  sorting, pagination, language fallback, and related-content resolution, so the
  results are identical to api mode over the same data.

### Configuration

```typescript
import { createClient } from '@content-island/api-client';

const client = createClient({
  accessToken: '<your-read-token>',
  mode: 'snapshot',
  // snapshotPath omitted — defaults to DEFAULT_SNAPSHOT_PATH ('./content-island-snapshot.json')
});
```

- **`accessToken`** is required in **all** modes. (In snapshot mode it still
  identifies the client; reads do not use the network, but the token is part of
  the client contract.)
- **`mode`** defaults to `'api'`. Set it to `'snapshot'` to serve reads from the
  snapshot.
- **`snapshotPath`** is **client-level** and **independent of `mode`**: it points
  at the snapshot file. It is **optional** — when omitted it defaults to
  `DEFAULT_SNAPSHOT_PATH` (`'./content-island-snapshot.json'`), a constant exported
  from the package and shared with the CLI's `--snapshot-path` default. An
  `'api'`-mode client may still set `snapshotPath` (for example, to expose
  `getSnapshotInfo()` while reading live).

```typescript
import { createClient, DEFAULT_SNAPSHOT_PATH } from '@content-island/api-client';

// Zero-config: export to the default location, then read it back with no path.
//   npx content-island export --access-token <your-read-token>
// writes ./content-island-snapshot.json, which this client resolves automatically:
const client = createClient({ accessToken: '<your-read-token>', mode: 'snapshot' });
```

`snapshotPath` (and the `DEFAULT_SNAPSHOT_PATH` default) is **cwd-relative** — it is
resolved against the process working directory at read time. In an SSR/serverless
runtime where the cwd is not the snapshot's location, pass an absolute path, e.g.
`snapshotPath: path.resolve(process.cwd(), 'content-island-snapshot.json')`.

If a read resolves to `'snapshot'` mode on a client created **without**
`snapshotPath`, it loads `DEFAULT_SNAPSHOT_PATH` — there is no "snapshotPath
required" error. If no readable, valid snapshot exists at the resolved path, the
load rejects with an `ApiClientError` whose message names that path. On a
snapshot-mode client that also configures a `snapshotLoader`, that error
additionally states that the loader is not used for the initial load and points at
`refreshSnapshot()` — the extra advice appears exactly when `refreshSnapshot()`
would work, so an `'api'`-mode client carrying a loader keeps the plain message.

The initial load **always** reads that file. Configuring a `snapshotLoader` does
not change it — see [Refreshing a snapshot](#refreshing-a-snapshot).

### Refreshing a snapshot

A snapshot-mode client can pull fresher content at runtime through an optional
`snapshotLoader` — any async function returning snapshot JSON text or an
already-parsed `ContentSnapshot`. The package's own
[`exportSnapshot`](#programmatic-exportsnapshot) already resolves a parsed
`ContentSnapshot`, so it _is_ a `SnapshotLoader` — pulling from Content Island
directly needs no infrastructure of your own:

```typescript
import { createClient, exportSnapshot } from '@content-island/api-client';

const accessToken = '<your-read-token>';

const client = createClient({
  accessToken,
  mode: 'snapshot',
  snapshotLoader: () => exportSnapshot({ accessToken }),
});
```

The same read token works — the export endpoint derives the project from it, like
every other read. Omit `exportSnapshot`'s `snapshotPath` here: the refresh keeps the
pulled snapshot in memory, and writing the file is the
[export step](#exporting-a-snapshot)'s job.

**The export endpoint is rate limited — 5 requests per 60-second window by
default**, keyed by project. Every token of a project shares that budget, so it is a
property of your project rather than of a single process: `refreshSnapshot()` on a
per-request SSR path, or across a parallel SSG build farm, will exhaust it.
Refreshing on a schedule or from a publish webhook stays comfortably inside it.

Exhausting it costs freshness, not availability: the `429` maps to `RATE_LIMITED`,
the loader rejects, and `refreshSnapshot()` rejects with it — but the active
snapshot is untouched, so reads keep serving it once you have caught the error
(see below).

If you do need frequent refreshes, publish the snapshot yourself and load it from a
CDN — those reads never touch the export budget:

```typescript
const client = createClient({
  accessToken: '<your-read-token>',
  mode: 'snapshot',
  snapshotLoader: () => fetch('https://cdn.example.com/snapshot.json').then(r => r.text()),
});
```

The loader is **only** invoked by `refreshSnapshot()` — never during
`createClient`, and never on the initial load. Publishing the snapshot file is the
consumer's responsibility; the library's job at startup is just to load it.

```typescript
const { status, meta } = await client.refreshSnapshot();
// status === 'updated'   → adopted; reads now serve the pulled snapshot (meta is its meta)
// status === 'unchanged' → the pulled snapshot was not newer; the current one is retained
```

`refreshSnapshot()` validates the pulled snapshot (shape and `schemaVersion`),
checks it against the active one (same `projectId` and `view`, newer
`exportedAt`), and swaps it in atomically — a read in flight during a refresh sees
either the whole old snapshot or the whole new one, never a mix. Overlapping calls
collapse onto a single pull.

**It rejects when the pull fails.** A loader rejection, invalid JSON, a bad shape, a
`schemaVersion` mismatch and an identity mismatch all throw an `ApiClientError`, as
does misconfiguration (an `'api'`-mode client, or a snapshot client with no
`snapshotLoader`). A failed pull never damages what you already have: the active
snapshot is untouched, so reads keep serving it as soon as you have caught the
error. **You have to catch it** — see below.

#### Refreshing in the background, without taking the server down

Awaiting a refresh on the request path costs your users the latency of a network
pull, so the natural call site is a background refresh — fire it and let the request
carry on. Fire it _unattended_, though, and a failed pull becomes an unhandled
promise rejection, which on Node >= 15 terminates the process: a blip at Content
Island takes your SSR server down. Always attach a `.catch()`:

```typescript
// WRONG — an unhandled rejection when Content Island is unreachable. The process exits.
client.refreshSnapshot();

// CORRECT — the refresh runs in the background; a failure is a log line, nothing more.
void client.refreshSnapshot().catch(error => {
  console.warn('Content snapshot refresh failed; still serving the previous snapshot.', error);
});
```

`void` is there to say the promise is deliberately not awaited; the `.catch()` is
what keeps the process alive. If you do `await` the call instead, wrap it in a
`try`/`catch` — same requirement, different shape.

#### If you have no snapshot on disk

**Ship the file.** Export the snapshot at build time so it is inside the deployed
artifact, and everything else follows: the initial load succeeds from disk, the
first render paints without waiting for anything, and `refreshSnapshot()` is purely
an update mechanism for later requests.

```jsonc
// package.json — the CLI reads the token from CONTENT_ISLAND_ACCESS_TOKEN,
// so it never appears in the command line or the build log.
{
  "scripts": {
    "build": "content-island export && <your build command>",
  },
}
```

Set `CONTENT_ISLAND_ACCESS_TOKEN` in the build environment, or pass
`--access-token <your-read-token>` explicitly. See
[the CLI](#cli-content-island-export) for every flag.

The alternative — bootstrapping the active snapshot from the loader at runtime — is
a fallback, not a recommendation, because **a read does not wait for an in-flight
refresh**. In an SSR framework the client is usually a module-level singleton while
route loaders run per request, independently of your startup code. If a loader reads
before the bootstrap has settled, it hits the initial load, finds no file and
throws: the first render dies and a reload appears to fix it, because by then the
refresh has completed. The file read fails in microseconds while the network pull
takes hundreds of milliseconds, so the read reliably loses that race.

Blocking the module graph with a top-level `await` to close that gap trades the
problem for a slower first paint and a server start that now depends on Content
Island being reachable. Shipping the file has neither cost.

If you do bootstrap from the loader, `refreshSnapshot()` must complete before the
first read, and it rejects if the pull fails — there is nothing on disk to fall back
to:

```typescript
const client = createClient({ accessToken: '<your-read-token>', mode: 'snapshot', snapshotLoader });

await client.refreshSnapshot(); // rejects if the pull fails: nothing else can serve reads
const posts = await client.getContentList<Post>({ contentType: 'post' });
```

That first refresh logs one warning, because adopting without a baseline also means
the `projectId`/`view` identity guard cannot run. It is expected on this route — and
it is also how you find out that a snapshot file you _did_ expect to be there is
corrupt rather than absent, since the warning names the path.

#### Working without a token, or without Content Island

**Snapshot-mode reads never touch the network.** They serve a file parsed into
memory, so neither the access token nor Content Island's availability affects them.
Both matter only when `refreshSnapshot()` runs.

That makes the intended development setup work: commit a snapshot to the repository,
work offline with no token, and **leave your production `snapshotLoader` and
`refreshSnapshot()` calls in the code**. Reads serve the committed file; the refresh
fails and is caught, so the same source runs in both places:

```typescript
const accessToken = process.env.CONTENT_ISLAND_TOKEN ?? ''; // empty in local development

const client = createClient({
  accessToken,
  mode: 'snapshot',
  snapshotLoader: () => exportSnapshot({ accessToken }),
});

await client.getContentList<Post>({ contentType: 'post' }); // reads the committed file

// Offline this rejects on every run, so say so once and carry on — never `.catch(() => {})`,
// which would also hide a genuine failure in production.
void client.refreshSnapshot().catch(error => {
  console.warn('Content snapshot refresh failed; still serving the committed snapshot.', error);
});
```

The `.catch()` is what makes that offline setup survivable — an unattended
`refreshSnapshot()` with no token would exit the process on the first tick.

`accessToken` is required by the `Options` type even though snapshot reads never
read it, so pass `''` (or an env fallback) to satisfy TypeScript — there is no
runtime validation of it.

### Per-query mode override

The five content reads — `getContentList`, `getContent`, `getRawContentList`,
`getRawContent`, and `getContentListSize` — accept a per-query `mode` that takes
precedence over the client-level mode **for that call only**:

```
effective mode = per-query mode ?? client-level mode ?? 'api'
```

```typescript
// An api-mode client that occasionally reads from a snapshot:
const client = createClient({
  accessToken: '<your-read-token>',
  snapshotPath: './content-island-snapshot.json',
});

const live = await client.getContentList<Post>({ contentType: 'post' }); // network
const fromSnapshot = await client.getContentList<Post>({
  contentType: 'post',
  mode: 'snapshot', // this call only — served from the snapshot, no network
});
```

```typescript
// A snapshot-mode client that occasionally needs fresh data:
const client = createClient({
  accessToken: '<your-read-token>',
  mode: 'snapshot',
  snapshotPath: './content-island-snapshot.json',
});

const fromSnapshot = await client.getContentList<Post>({ contentType: 'post' }); // snapshot
const live = await client.getContentList<Post>({
  contentType: 'post',
  mode: 'api', // this call only — hits the network
});
```

`getProject` is routed by the **client-level** mode only — it takes no query
params, so there is no per-query override.

`mode` is a client-only key: it is never serialized into the request query string
in api mode.

### Related-content metadata: `onRelatedContentMeta`

The five content reads also accept an optional per-query callback,
`onRelatedContentMeta`, invoked exactly once with the related-content resolution
metadata for that call:

```typescript
await client.getContentList<Post>({
  contentType: 'post',
  includeRelatedContent: 'all',
  onRelatedContentMeta: ({ resolvedDepth, partial }) => {
    // resolvedDepth: how deep the related-content BFS actually resolved
    // partial: true when a depth or budget cap left part of the graph unresolved
    console.log({ resolvedDepth, partial });
  },
});
```

It works in **both** modes — sourced from the `X-Related-Content-Resolved-Depth` /
`X-Related-Content-Partial` response headers in api mode, and from the engine's
BFS result in snapshot mode, with identical values for the same data and query.
When the callback is omitted, behavior and return shapes are unchanged. Like
`mode`, it is never serialized into the request query string.

### Writes are not available in snapshot mode

A snapshot-mode client serves reads only. Every write/management method
(`createContent`, `publishContent`, `updateContentFieldValue`, `uploadMedia`,
`createModel`, `updateModel`, `deleteModel`, `createEnum`, `updateEnum`,
`deleteEnum`) rejects with an `ApiClientError` (code `SNAPSHOT_MODE`) and performs
no network call. There is no per-query override for writes — on a snapshot-mode client
they always reject. If you need to read from a snapshot **and** write, create a
second, separate api-mode client for the writes:

```typescript
const reader = createClient({
  accessToken: '<your-read-token>',
  mode: 'snapshot',
  snapshotPath: './content-island-snapshot.json',
});

const writer = createClient({ accessToken: '<your-write-token>' }); // api mode
```

### `getSnapshotInfo()`

`getSnapshotInfo()` resolves with the snapshot's `meta` block — useful for
freshness checks (e.g. logging when the snapshot was produced during a build):

```typescript
const meta = await client.getSnapshotInfo();
// {
//   schemaVersion: 1,
//   exportedAt: '2026-06-12T10:00:00.000Z', // ISO-8601
//   projectId: '...',
//   view: 'published' | 'preview',
// }
```

It works on **any** client, regardless of the client-level mode (so an api-mode
client can report on the snapshot it has on disk). On a client without
`snapshotPath`, it loads `DEFAULT_SNAPSHOT_PATH`; if no readable, valid snapshot
exists at the resolved path, it rejects with an `ApiClientError` whose message
names that path.

## Exporting a snapshot

### CLI: `content-island export`

The package ships a `content-island` binary that downloads a snapshot and writes
it to disk as plain JSON:

```bash
npx content-island export --access-token <your-read-token> --snapshot-path ./content-island-snapshot.json
```

> **Note:** The `content-island` binary lives inside the `@content-island/api-client`
> package. Inside a workflow (e.g. a CI job or an npm script of a project that already
> depends on `@content-island/api-client`), `npx content-island` resolves the binary
> from the installed package automatically. To run it standalone outside such a context,
> install the package globally first so the command is available on your `PATH`:
>
> ```bash
> npm install -g @content-island/api-client
> content-island export --access-token <your-read-token>
> ```

The flags map **1:1** (kebab-case) onto the matching [`exportSnapshot`](#programmatic-exportsnapshot) option:

| Flag                   | `exportSnapshot` option | Type    | Description                                                                                    | Default                          |
| ---------------------- | ----------------------- | ------- | ---------------------------------------------------------------------------------------------- | -------------------------------- |
| `--access-token`       | `accessToken`           | string  | Access token (required). Falls back to the `CONTENT_ISLAND_ACCESS_TOKEN` environment variable. | —                                |
| `--snapshot-path`      | `snapshotPath`          | string  | Path to write the snapshot JSON.                                                               | `./content-island-snapshot.json` |
| `--domain`             | `domain`                | string  | Target domain (self-hosted/staging). Respects the client's default-domain resolution.          | client default                   |
| `--no-secure-protocol` | `secureProtocol`        | boolean | Use **HTTP** instead of HTTPS (for local/self-hosted http targets).                            | HTTPS (secure)                   |
| `--secure-protocol`    | `secureProtocol`        | boolean | Use **HTTPS** explicitly (redundant — this is already the default).                            | HTTPS (secure)                   |
| `--api-version`        | `apiVersion`            | string  | API version segment to target.                                                                 | client default                   |

The request is **HTTPS by default**. Pass `--no-secure-protocol` to use plain
HTTP when targeting a local or self-hosted http endpoint:

```bash
# Export from a local http instance (e.g. a dev server on localhost):
npx content-island export \
  --access-token <your-read-token> \
  --domain localhost:8082 \
  --no-secure-protocol \
  --snapshot-path ./content-island-snapshot.json
```

The exported **view is token-driven**: a `PREVIEW_`-prefixed token exports the
preview (draft) view, any other token exports the published view. There is no
view flag.

On success the CLI prints a summary and exits `0`:

```
Content snapshot exported successfully.
  Output:     ./content-island-snapshot.json
  Size:       1.42 MB
  Exported:   2026-06-12T10:00:00.000Z
  View:       published
```

If the token is missing, or the request/validation fails, the CLI writes the
error to stderr and exits non-zero — and because the write goes through a
temp-file-then-rename, no partial or invalid file is left at `--snapshot-path`.

### Programmatic: `exportSnapshot`

`exportSnapshot` is the function the CLI is built on; use it directly from a Node
script when you need more control:

```typescript
import { exportSnapshot } from '@content-island/api-client';

const snapshot = await exportSnapshot({
  accessToken: process.env.CONTENT_ISLAND_ACCESS_TOKEN!,
  snapshotPath: './content-island-snapshot.json', // optional — omit to just get the parsed snapshot
  // domain, secureProtocol, apiVersion are also accepted
});

console.log(snapshot.meta.exportedAt, snapshot.contents.length);
```

It fetches the snapshot, validates its shape and schema version, optionally writes
it to `snapshotPath` (same safe temp-then-rename strategy as the CLI), and resolves
with the parsed `ContentSnapshot`. A non-2xx response (including `429`, which maps
to code `RATE_LIMITED`) rejects with the standard `ApiClientError`.

### Size guidance

The snapshot is a single JSON document loaded fully into memory by the snapshot-mode
client. The practical limit is around **20 MB of uncompressed JSON on disk**.
Above that threshold the CLI prints a warning (it still exits `0`): snapshots this
large are loaded entirely into memory and may approach upstream request/body and
timeout limits. If you cross it, consider narrowing the exported content or
keeping the snapshot well under the threshold.

## Recommended workflow

Use **`mode: 'api'`** in local development (always fresh, no export step) and
**`mode: 'snapshot'`** in production builds (fast, no per-request network calls).
Switch between them with an environment variable so the same code runs in both:

```typescript
import { createClient } from '@content-island/api-client';

const client = createClient({
  accessToken: process.env.CONTENT_ISLAND_ACCESS_TOKEN!,
  mode: process.env.NODE_ENV === 'production' ? 'snapshot' : 'api',
  snapshotPath: './content-island-snapshot.json',
});
```

In production builds, generate the snapshot first (e.g. as a build step) and then
run the build, which reads from it in snapshot mode.

### GitHub Action snippet

This step exports the snapshot with the CLI before building the site:

```yaml
name: Build site

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: 24

      - name: Install dependencies
        run: npm ci

      - name: Export content snapshot
        env:
          CONTENT_ISLAND_ACCESS_TOKEN: ${{ secrets.CONTENT_ISLAND_ACCESS_TOKEN }}
        run: npx content-island export

      - name: Build (snapshot mode)
        env:
          NODE_ENV: production
        run: npm run build
```

The token is read from `CONTENT_ISLAND_ACCESS_TOKEN`, so it never appears in the command
line or logs. The build then reads from `./content-island-snapshot.json` in snapshot mode.

## Documentation

For more detailed documentation, please refer to the [Content Island API Client documentation](https://docs.contentisland.net/client-api/overview/).

---
_Source: https://npm.io/package/@content-island/api-client · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
