---
title: @storyblok/api-client Reference
description: @storyblok/api-client is Storyblok's modern TypeScript client for the Content Delivery API, with built-in caching, retry logic, rate limiting, and typed responses.
url: https://www.storyblok.com/docs/libraries/js/content-delivery-api-client
---

# @storyblok/api-client Reference

[@storyblok/api-client](https://github.com/storyblok/monoblok/tree/main/packages/capi-client) is Storyblok’s modern TypeScript client for the Content Delivery API (CAPI). It exposes a single `createApiClient()` factory that returns resource clients for stories, links, tags, datasources, datasource entries, spaces, and experiments, with built-in caching, retry logic, and rate limiting. Responses are fully typed and can be narrowed end-to-end by passing a project schema type from [`@storyblok/schema`](/docs/libraries/js/schema) to `.withTypes<Schema>()`.

> [!WARNING]
> This reference documents the **v1 alpha** release of `@storyblok/api-client`. The API may change before the stable `1.0.0` release. Install the alpha explicitly via the `@alpha` dist-tag.

## Requirements

-   **Node.js** LTS (version 22.x recommended)
-   **Modern web browser** (the latest versions of Chrome, Edge, Firefox, and Safari)

## Installation

Add the package to a project:

```bash
npm install @storyblok/api-client@alpha
```

## Usage

The following example demonstrates how to initialize the client and fetch a story from the Content Delivery API:

```ts
import { createApiClient } from "@storyblok/api-client";

const client = createApiClient({
  accessToken: process.env.STORYBLOK_ACCESS_TOKEN!,
  region: "eu",
});

const { data, error } = await client.stories.get("home");
if (error) throw error;

console.log(data?.story.content);
```

Every resource client method returns an `ApiResponse` object containing `data`, `error`, `response`, and `request`. Set `throwOnError: true` on the client (or per request) to receive `data` directly and have errors thrown as `ClientError` instances instead.

## API

The following sections document the `createApiClient()` factory, each resource client, and the exported error class and types.

### `createApiClient()`

Accepts a configuration object and returns a client with typed methods for each Content Delivery API resource.

```js
createApiClient(config);
```

```js
import { createApiClient } from "@storyblok/api-client";

const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  region: "us",
});
```

`createApiClient()` accepts the following options:

| Option | Description |
| --- | --- |
| `accessToken` | **Required.** The Storyblok access token (preview or public). |
| `region` | The API region. Possible values: `'eu'` (default), `'us'`, `'ap'`, `'ca'`, `'cn'`. See [Region](#region). |
| `baseUrl` | A custom base URL that overrides the region-based URL (for example, when proxying requests). |
| `headers` | Additional headers to send with every request. |
| `throwOnError` | If `true`, the client throws on HTTP errors instead of returning `{ error }`. Defaults to `false`. |
| `inlineRelations` | If `true`, the client replaces resolved relation UUIDs in story content with full story objects. Defaults to `false`. See [Inline relations](#inline-relations). |
| `cache` | Cache configuration object. See [Caching](#caching). |
| `retry` | Retry configuration object. See [Retry](#retry). |
| `rateLimit` | Rate-limiting configuration. See [Rate limiting](#rate-limiting). |
| `timeout` | Request timeout in milliseconds. Defaults to `30_000`. |
| `fetch` | A custom `fetch` implementation to use instead of the global `fetch`. Useful for testing or platform-specific fetch wrappers. |

The returned client exposes the following properties:

| Property | Description |
| --- | --- |
| `stories` | Methods for fetching stories. See [`client.stories`](#clientstories). |
| `links` | Methods for fetching links. See [`client.links`](#clientlinks). |
| `spaces` | Methods for fetching space information. See [`client.spaces`](#clientspaces). |
| `datasources` | Methods for fetching datasources. See [`client.datasources`](#clientdatasources). |
| `datasourceEntries` | Methods for fetching datasource entries. See [`client.datasourceEntries`](#clientdatasourceentries). |
| `tags` | Methods for fetching tags. See [`client.tags`](#clienttags). |
| `experiments` | Methods for fetching experiments. See [`client.experiments`](#clientexperiments). |
| `get` | A generic `GET` method for arbitrary Content Delivery API endpoints. See [`client.get()`](#clientget). |
| `flushCache` | Flushes the cache and resets the tracked content version. See [`client.flushCache()`](#clientflushcache). |
| `interceptors` | Request, response, and error interceptors. See [`client.interceptors`](#clientinterceptors). |
| `withTypes` | Returns a typed client that narrows `story.content` to a project schema. See [TypeScript](#typescript). |

#### Region

Use the region option to match the space’s server location.

```js
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  region: "us",
});
```

The following options are available:

| Value | Region |
| --- | --- |
| `'eu'` | European Union (default). |
| `'us'` | United States. |
| `'ap'` | Australia/Asia Pacific. |
| `'ca'` | Canada. |
| `'cn'` | China. |

> [!WARNING]
> The `region` parameter is required for non-EU spaces.

### `client.stories`

Use the `stories` resource client to retrieve stories.

#### `client.stories.get()`

Fetches a single story by slug, ID, or UUID. A UUID identifier sets `find_by: 'uuid'` automatically.

```js
client.stories.get(identifier);
client.stories.get(identifier, options);
```

```js
// Fetch by slug
const result = await client.stories.get("blog/my-first-post", {
  query: { version: "draft" },
});

// Fetch by ID
const result = await client.stories.get(123456);

// Fetch by UUID (find_by is set automatically)
const result = await client.stories.get("2bd955a2-6e1f-4d3c-9a0d-3f1b8c7e4a56");
```

The `identifier` parameter accepts the following values:

| Type | Description |
| --- | --- |
| `string` | A story’s `full_slug` (for example, `'blog/my-post'`) or `uuid`. A `uuid` sets `find_by: 'uuid'` automatically. |
| `number` | A story’s numeric `id`. |

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following properties. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/stories/retrieve-a-single-story#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `find_by` | Set to `'uuid'` to look up a story by UUID. Set automatically for UUID identifiers. |
| `version` | `'published'` or `'draft'`. |
| `cv` | Cache version number. |
| `from_release` | Retrieve content from a specific release by ID. |
| `resolve_links` | Resolve link fields: `'story'`, `'url'`, or `'link'`. |
| `resolve_links_level` | Resolve link nesting depth: `1` or `2`. |
| `resolve_relations` | Comma-separated list of `component.field` pairs to resolve. |
| `resolve_level` | Set to `2` to resolve nested stories. |
| `resolve_assets` | Set to `1` to include asset metadata. |
| `fallback_lang` | Fallback language code. |
| `language` | Language code for localized content. |

#### `client.stories.list()`

Fetches multiple stories with optional filtering, sorting, and pagination.

```js
client.stories.list();
client.stories.list(options);
```

```js
const result = await client.stories.list({
  query: {
    starts_with: "blog/",
    version: "draft",
    per_page: 25,
  },
});

if (result.data) {
  console.log(result.data.stories);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts all properties from `client.stories.get()` (except `find_by`) plus the following. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/stories/retrieve-multiple-stories#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `starts_with` | Filter stories by path prefix. |
| `search_term` | Full-text search term. |
| `per_page` | Number of stories per page. |
| `page` | Page number for pagination. |
| `content_type` | Filter by component name. |
| `by_slugs` | Comma-separated list of slugs. |
| `excluding_slugs` | Comma-separated list of slugs to exclude. |
| `by_uuids` | Comma-separated list of UUIDs. |
| `by_uuids_ordered` | Comma-separated list of UUIDs (results preserve order). |
| `excluding_ids` | Comma-separated list of IDs to exclude. |
| `with_tag` | Filter by tag name. |
| `sort_by` | Sort field and direction (for example, `'published_at:desc'`). |
| `in_workflow_stages` | Filter by workflow stage IDs. |
| `filter_query` | Filter query object. |
| `level` | Story nesting level. |
| `is_startpage` | Set to `1` for start pages only, `0` to exclude them. |
| `first_published_at_gt` | Filter by first published date (greater than). |
| `first_published_at_lt` | Filter by first published date (less than). |
| `published_at_gt` | Filter by published date (greater than). |
| `published_at_lt` | Filter by published date (less than). |
| `updated_at_gt` | Filter by updated date (greater than). |
| `updated_at_lt` | Filter by updated date (less than). |
| `excluding_fields` | Comma-separated list of fields to exclude from the response. |

### `client.links`

Use the `links` resource client to retrieve links.

#### `client.links.list()`

Fetches all links from the Content Delivery API. Useful for building site-wide navigation menus.

```js
client.links.list();
client.links.list(options);
```

```js
const result = await client.links.list({
  query: { version: "published" },
});

if (result.data) {
  console.log(result.data.links);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following properties. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/links/retrieve-multiple-links#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `version` | `'published'` or `'draft'`. |
| `cv` | Cache version number. |
| `starts_with` | Filter links by path prefix. |
| `with_parent` | Filter by parent folder ID. |
| `include_dates` | Set to `1` to include date fields. |
| `paginated` | Set to `1` to enable pagination. |
| `page` | Page number. |
| `per_page` | Number of links per page. |

### `client.spaces`

Use the `spaces` resource client to retrieve information about the space.

#### `client.spaces.get()`

Fetches public information about the current space (name, language codes, version `cv`). This endpoint is never cached.

```js
client.spaces.get();
client.spaces.get(options);
```

```js
const result = await client.spaces.get();

if (result.data) {
  console.log(result.data.space);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters forwarded to the request. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

### `client.datasources`

Use the `datasources` resource client to retrieve datasources.

#### `client.datasources.get()`

Fetches a single datasource by numeric ID. Resolve the ID via `client.datasources.list()` if only the slug is known.

```js
client.datasources.get(id);
client.datasources.get(id, options);
```

```js
const result = await client.datasources.get(123);

if (result.data) {
  console.log(result.data.datasource);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following property. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/datasources/retrieve-a-single-datasource#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `cv` | Cache version number. |

#### `client.datasources.list()`

Fetches all datasources.

```js
client.datasources.list();
client.datasources.list(options);
```

```js
const result = await client.datasources.list({
  query: { page: 1, per_page: 25 },
});

if (result.data) {
  console.log(result.data.datasources);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following properties. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/datasources/retrieve-multiple-datasources#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `page` | Page number. |
| `per_page` | Number of datasources per page. |
| `cv` | Cache version number. |

### `client.datasourceEntries`

Use the `datasourceEntries` resource client to retrieve the entries of a datasource.

#### `client.datasourceEntries.list()`

Fetches all datasource entries.

```js
client.datasourceEntries.list();
client.datasourceEntries.list(options);
```

```js
const result = await client.datasourceEntries.list({
  query: { datasource: "categories" },
});

if (result.data) {
  console.log(result.data.datasource_entries);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following properties. For the complete list, see [Query parameters](/docs/api/content-delivery/v2/datasources/retrieve-multiple-datasource-entries#query-parameters) in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `datasource` | The datasource slug. |
| `dimension` | The datasource dimension value. |
| `page` | Page number. |
| `per_page` | Number of entries per page. |
| `cv` | Cache version number. |

### `client.tags`

Use the `tags` resource client to retrieve tags.

#### `client.tags.list()`

Fetches all tags.

```js
client.tags.list();
client.tags.list(options);
```

```js
const result = await client.tags.list({
  query: { starts_with: "blog" },
});

if (result.data) {
  console.log(result.data.tags);
}
```

The `options` parameter accepts the following properties:

| Property | Description |
| --- | --- |
| `query` | Query parameters. See the table below. |
| `signal` | An `AbortSignal` for canceling the request. |
| `throwOnError` | Override the client-level `throwOnError` setting for this request. |
| `fetchOptions` | Custom options forwarded to the underlying `fetch` call. See [Custom fetch options](#custom-fetch-options). |

The `query` object accepts the following properties. For the complete list, see the [Tags](/docs/api/content-delivery/v2/tags) endpoints in the Content Delivery API reference.

| Property | Description |
| --- | --- |
| `starts_with` | Filter tags by prefix. |
| `version` | `'published'` or `'draft'`. |

### `client.experiments`

Use the `experiments` resource client to retrieve running experiments and their variant mappings.

#### `client.experiments.list()`

Returns all running experiments. Each experiment lists its variants and their `story_mappings`, which map an `original_story_id`/`original_slug` to a `variant_story_id`/`variant_slug`.

```js
client.experiments.list();
client.experiments.list(options);
```

```js
const result = await client.experiments.list();

if (result.data) {
  console.log(result.data.experiments);
}
```

The Content Delivery API has no per-story variant filter: list the experiments, pick a variant deterministically, then fetch that variant’s `variant_slug` via `client.stories`. For the underlying endpoints, refer to [Experiments](/docs/api/content-delivery/v2/experiments) in the Content Delivery API reference.

### `client.get()`

A generic `GET` method for arbitrary Content Delivery API endpoints. The caller provides the expected response type as a generic parameter.

```js
client.get(path);
client.get(path, options);
```

```typescript
const result = await client.get<{ links: Record<string, unknown> }>("/v2/cdn/links", { query: { version: "published" } });
```

### `client.flushCache()`

Flushes the cache and resets the tracked content version (`cv`). Intended for use when `cache.flush` is `'manual'` — for example, after receiving a Storyblok webhook.

```js
client.flushCache();
```

### `client.interceptors`

The `interceptors` property exposes request, response, and error middleware.

```js
client.interceptors.request.use((request, options) => {
  // Modify or inspect outgoing requests.
  return request;
});

client.interceptors.response.use((response, request, options) => {
  // Modify or inspect incoming responses.
  return response;
});

client.interceptors.error.use((error, response, request, options) => {
  // Handle or transform errors.
  return error;
});
```

Each interceptor channel (`request`, `response`, `error`) supports the following methods:

| Method | Description |
| --- | --- |
| `use(fn)` | Register an interceptor. Returns an ID. |
| `eject(id)` | Remove an interceptor by ID. |
| `exists(id)` | Check whether an interceptor is registered. |
| `update(id, fn)` | Replace an interceptor. |
| `clear()` | Remove all interceptors. |

#### Example: Add a custom header to every request

```js
client.interceptors.request.use((request, options) => {
  request.headers.set("X-Custom-Header", "my-value");
  return request;
});
```

### `ClientError`

An error class representing an HTTP error response. Thrown when `throwOnError` is `true`, or available on the `error` property of the response when `throwOnError` is `false`.

```js
import { ClientError } from "@storyblok/api-client";

new ClientError(message, { status, statusText, data });
```

`ClientError` exposes the error details under a single `response` property:

| Property | Type | Description |
| --- | --- | --- |
| `message` | `string` | The error message. |
| `response.status` | `number` | The HTTP status code. |
| `response.statusText` | `string` | The HTTP status text. |
| `response.data` | `ApiErrorBody \| undefined` | The parsed response body (typically `{ error?, message? }`). |

## Inline relations

[Developer Concept: References](/docs/concepts/references)Refer to the references documentation to learn how references are resolved in Storyblok.

By default, references fields remain UUID strings even when `resolve_relations` is used. Set `inlineRelations: true` to replace matching relation fields in story content with full story objects.

```js
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  inlineRelations: true,
});

const result = await client.stories.get("blog/my-post", {
  query: {
    version: "draft",
    resolve_relations: "article.author",
  },
});
```

When enabled:

-   Inlining runs only for fields explicitly listed in `resolve_relations` (exact `component.field` match).
-   The `rels` and `rel_uuids` arrays remain unchanged in the response payload.
-   If the API response includes `rel_uuids` (relation overflow), the client automatically fetches the missing related stories and includes them in the inlining pass.
-   Relation fields can contain either UUID strings or inlined story objects.
-   Cyclic relations produce cyclic object references (for example, `A → B → A`).

Before (`inlineRelations: false`):

```js
result.data.story.content.author;
// "9ef7f0c0-5a5f-4fbb-9f5e-1e8de56b8910"
```

After (`inlineRelations: true`):

```js
result.data.story.content.author;
// { uuid: "9ef7f0c0-5a5f-4fbb-9f5e-1e8de56b8910", name: "...", ...storyFields }
```

> [!WARNING]
> `inlineRelations: true` can produce cyclic object references. This can break serialization in frameworks that rely on `JSON.stringify` internally — most notably **Next.js**, where passing cyclic data from Server Components to Client Components or returning it from `getServerSideProps` fails (see [next.js#72189](https://github.com/vercel/next.js/issues/72189)).
> 
> Frameworks that use [`devalue`](https://github.com/Rich-Harris/devalue) for serialization (Nuxt, SvelteKit) handle cyclic references automatically.
> 
> If your framework does not support cyclic references, you can use `devalue` yourself to serialize the response before passing it across the server-client boundary:
> 
> ```js
> // app/page.tsx (Next.js Server Component)
> import { stringify } from "devalue";
> import { createApiClient } from "@storyblok/api-client";
> import { Stories } from "./stories";
> 
> // `inlineRelations` is a client-level option. Set it when creating the client.
> const client = createApiClient({ accessToken: "<your-access-token>", inlineRelations: true });
> 
> export default async function Page() {
>   const result = await client.stories.list({
>     query: { resolve_relations: "article.author", version: "draft" },
>   });
> 
>   // Serialize with devalue to safely handle cyclic references.
>   const serialized = stringify(result.data);
> 
>   return <Stories data={serialized} />;
> }
> ```
> 
> ```js
> // app/stories.tsx (Next.js Client Component)
> "use client";
> import { parse } from "devalue";
> 
> export function Stories({ data }) {
>   const { stories } = parse(data);
>   // `stories` now contains the fully inlined (and potentially cyclic) object graph.
>   return (
>     <ul>
>       {stories.map((s) => (
>         <li key={s.uuid}>{s.name}</li>
>       ))}
>     </ul>
>   );
> }
> ```
> 
> Alternatively, leave `inlineRelations` disabled and access related stories via the `rels` array in the response to avoid cyclic references entirely.

## Caching

[Developer Concept: Caching](/docs/concepts/caching)Refer to the caching documentation for an overview on caching in Storyblok.

The client includes a built-in in-memory cache for published (`version: 'published'`) `GET` requests. Draft requests (`version: 'draft'`) and the spaces endpoint are never cached. The default cache is an in-memory LRU (1000 entries) with the `'cache-first'` strategy and a 60-second TTL.

```js
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  cache: {
    strategy: "cache-first",
    ttlMs: 60_000,
  },
});
```

The `cache` object accepts the following options:

| Option | Description |
| --- | --- |
| `provider` | A custom cache provider. Defaults to an in-memory LRU cache (`maxEntries: 1_000`). See [Custom cache provider](#custom-cache-provider). |
| `strategy` | A cache strategy name or custom handler function. Defaults to `'cache-first'`. See [Cache strategies](#cache-strategies). |
| `ttlMs` | Cache entry time-to-live in milliseconds. Defaults to `60_000`. |
| `cv` | Controls the `cv` (content version) query parameter. `'auto'` (default) attaches the tracked `cv` to published requests for cache busting. `'manual'` omits it (the client still tracks `cv` internally), useful for SSR with edge caching where stable URLs are required. |
| `flush` | `'auto'` (default) flushes the cache when the API returns a new content version. `'manual'` requires calling `client.flushCache()` explicitly. See [Cache flush control](#cache-flush-control). |
| `onRevalidationError` | Called when SWR background revalidation fails. Only relevant when `strategy` is `'swr'`. Defaults to `console.warn`. |

### Cache strategies

| Strategy | Behavior |
| --- | --- |
| `'cache-first'` | The default. Serve from cache if available. Fall back to the network and cache the result. |
| `'network-first'` | Always fetch from the network. Cache the result and fall back to cache on error. |
| `'swr'` | Serve stale cache immediately and revalidate in the background. If no cached response exists, the request waits for the network result. |

The `strategy` option also accepts a custom handler function of type `CacheStrategyHandler`:

```typescript
import type { CacheStrategyHandler } from "@storyblok/api-client";

const customStrategy: CacheStrategyHandler = async ({ key, cachedResult, loadNetwork }) => {
  if (cachedResult) {
    return cachedResult;
  }
  return loadNetwork();
};

const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  cache: { strategy: customStrategy },
});
```

### Cache flush control

By default (`flush: 'auto'`), the cache flushes automatically whenever the API returns a new content version (`cv`) value.

For at-scale scenarios that require external control over cache invalidation (for example, distributed Redis across multiple instances, webhook-triggered flush, or SSG build race conditions), set `flush: 'manual'` and call `client.flushCache()` explicitly.

```js
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  cache: { flush: "manual" },
});

// Flush manually after receiving a Storyblok webhook
app.post("/webhook", async (req, res) => {
  await client.flushCache();
  res.sendStatus(200);
});
```

### Custom cache provider

Provide a custom cache provider to integrate with Redis, Memcached, or any other caching backend. The provider must implement the `CacheProvider` interface:

```typescript
import type { CacheProvider } from "@storyblok/api-client";

const memory = new Map<string, { value: unknown; storedAt: number; ttlMs: number }>();

const customCacheProvider: CacheProvider = {
  async get(key) {
    return memory.get(key);
  },
  async set(key, entry) {
    memory.set(key, {
      value: entry.value,
      storedAt: entry.storedAt ?? Date.now(),
      ttlMs: entry.ttlMs,
    });
  },
  async flush() {
    memory.clear();
  },
};

const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  cache: {
    provider: customCacheProvider,
    strategy: "network-first",
    ttlMs: 30_000,
  },
});
```

The `CacheProvider` interface:

| Method | Signature | Description |
| --- | --- | --- |
| `get` | `(key: string) => Promise<CacheEntry \| undefined>` | Retrieve a cache entry by key. |
| `set` | `(key: string, entry: CacheEntryInput) => Promise<void>` | Store a cache entry. |
| `flush` | `() => Promise<void>` | Remove all cache entries. |

A `CacheEntry` contains:

| Property | Type | Description |
| --- | --- | --- |
| `value` | `unknown` | The cached value. |
| `storedAt` | `number` | Timestamp (milliseconds) when the entry was stored. |
| `ttlMs` | `number` | Time-to-live in milliseconds. |

## Retry

The client retries failed requests automatically. Pass a `retry` object to override individual settings — any omitted property falls back to its default.

| Property | Default | Description |
| --- | --- | --- |
| `limit` | `3` | Maximum number of retry attempts. |
| `backoffLimit` | `20_000` | Maximum backoff delay in milliseconds. |
| `jitter` | `true` | Add randomness to backoff delays to avoid thundering herd. |

```js
// Increase the retry limit and keep other defaults.
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  retry: { limit: 5 },
});

// Disable retries entirely.
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  retry: { limit: 0 },
});
```

## Rate limiting

The client applies preventive rate limiting by default to avoid hitting the Content Delivery API limits. Refer to the [rate limit documentation](/docs/api/content-delivery/v2/getting-started/rate-limit) for more details.

> [!NOTE]
> The `X-RateLimit-Policy` response header advertises a **concurrency** quota — the maximum number of in-flight requests — not a per-second request rate. The client uses it as a maximum cap on concurrent requests; exceeding it results in `429` responses.

The `rateLimit` option accepts several forms:

| Value | Behavior |
| --- | --- |
| `undefined` | The default. Auto-detect the concurrency tier from the request path and `per_page` query parameter. |
| `number` | Fixed maximum number of concurrent (in-flight) requests. |
| `RateLimitConfig` | Full configuration object (see below). |
| `false` | Disable rate limiting entirely. |

The `RateLimitConfig` object accepts:

| Property | Default | Description |
| --- | --- | --- |
| `maxConcurrency` | Auto-detected | Fixed maximum number of concurrent (in-flight) requests. When omitted (the default), the concurrency tier is auto-detected; setting it disables auto-detection. |
| `adaptToServerHeaders` | `true` | Dynamically adjust the concurrency limit from the `X-RateLimit-Policy` response header (a concurrency quota, not a request-rate limit). |

```js
// Disable rate limiting.
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  rateLimit: false,
});

// Fixed limit.
const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  rateLimit: 10,
});
```

## Custom fetch options

Every resource client method accepts a `fetchOptions` property for passing non-standard options to the underlying `fetch` call. This is useful for platform-specific extensions such as Next.js cache control or Cloudflare Workers settings.

The `fetchOptions` object is typed as `Record<string, unknown>`, so any key-value pair is accepted. The options are forwarded directly to the `fetch` call as the second argument alongside standard `RequestInit` properties.

The following examples show common cases:

### Next.js cache revalidation

```js
const result = await client.stories.get("home", {
  fetchOptions: {
    next: { revalidate: 60 },
  },
});
```

```js
const result = await client.stories.list({
  query: { starts_with: "blog/" },
  fetchOptions: {
    next: { revalidate: 120, tags: ["stories"] },
  },
});
```

### Cloudflare Workers

```js
const result = await client.stories.get("home", {
  fetchOptions: {
    cf: { cacheTtl: 300, cacheEverything: true },
  },
});
```

### Custom fetch implementation

The examples above forward options through the global `fetch`. To call the API with a _different_ `fetch` altogether, supply one via the `fetch` config option when creating the client. Combine the `fetch` config option with per-request `fetchOptions` for full control. For example, pass Next.js `fetch` at init time and control revalidation per request:

```js
import { createApiClient } from "@storyblok/api-client";

const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  fetch, // Next.js extended fetch
});

// This request revalidates every 60 seconds.
const home = await client.stories.get("home", {
  fetchOptions: { next: { revalidate: 60 } },
});

// This request uses on-demand revalidation via tags.
const blog = await client.stories.list({
  query: { starts_with: "blog/" },
  fetchOptions: { next: { tags: ["blog"] } },
});
```

## Error handling

By default, the client returns a response object with either `data` or `error`:

```js
import { createApiClient } from "@storyblok/api-client";

const client = createApiClient({ accessToken: "YOUR_ACCESS_TOKEN" });

const result = await client.stories.get("home");

if (result.data) {
  console.log(result.data.story);
} else {
  console.error(result.error);
}
```

Set `throwOnError: true` to throw a `ClientError` on HTTP errors instead:

```js
import { ClientError } from "@storyblok/api-client";

const client = createApiClient({
  accessToken: "YOUR_ACCESS_TOKEN",
  throwOnError: true,
});

try {
  const result = await client.stories.get("home");
  console.log(result.data.story);
} catch (error) {
  if (error instanceof ClientError) {
    console.error(error.response.status, error.response.statusText, error.response.data);
  }
}
```

## TypeScript

`@storyblok/api-client` ships complete type definitions and is fully compatible with `@storyblok/schema`.

### Usage with `@storyblok/schema`

Use the client in conjunction with `[@storyblok/schema](/docs/libraries/js/schema)` to narrow story.content to the project’s component union. The client object exposes a `withTypes<T>()` method that returns the same instance cast to a version that narrows story content to the components declared in the passed schema. `withTypes<T>()` accepts either `{ components: Block }` or `{ blocks: Block }`, the latter matches the Schema type produced by `@storyblok/schema`. It carries no runtime cost and adds no schema values to the bundle.

```ts
import { createApiClient } from "@storyblok/api-client";
import type { Schema } from "./schema";

const client = createApiClient({ accessToken: "YOUR_ACCESS_TOKEN" }).withTypes<Schema>();

const { data } = await client.stories.get("home");
// data.story.content is now a discriminated union of the schema's blocks.
```

## Pagination

-   [Previous: Universal API Client](/docs/libraries/js/universal-api-client)
-   [Next: Management API Client](/docs/libraries/js/management-api-client)
