> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mdcms.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK

> TypeScript client SDK for consuming the MDCMS Content API

The `@mdcms/sdk` package provides a type-safe TypeScript client for querying content from the MDCMS API. It handles authentication headers, response parsing, pagination, and error handling.

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @mdcms/sdk
  ```

  ```bash pnpm theme={null}
  pnpm add @mdcms/sdk
  ```

  ```bash yarn theme={null}
  yarn add @mdcms/sdk
  ```

  ```bash bun theme={null}
  bun add @mdcms/sdk
  ```
</CodeGroup>

## Client Setup

Create a client instance with your server URL, API key, project, and environment:

```typescript theme={null}
import { createClient } from "@mdcms/sdk";

const client = createClient({
  serverUrl: "https://cms.example.com",
  apiKey: process.env.MDCMS_API_KEY!,
  project: "marketing-site",
  environment: "production",
});
```

### Configuration Options

| Option        | Type     | Required | Description                                                                                                             |
| ------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `serverUrl`   | string   | Yes      | The base URL of your MDCMS server (e.g., `https://cms.example.com`).                                                    |
| `apiKey`      | string   | Yes      | API key with the appropriate scopes (prefixed with `mdcms_key_`).                                                       |
| `project`     | string   | Yes      | Project slug. Sets the `X-MDCMS-Project` header on every request.                                                       |
| `environment` | string   | Yes      | Environment name. Sets the `X-MDCMS-Environment` header on every request.                                               |
| `fetch`       | function | No       | Custom `fetch` implementation. Useful for testing, caching, or request instrumentation. Defaults to the global `fetch`. |

***

## Querying -- get()

The `get()` method retrieves a single document by ID, slug, or path. It returns the full `ContentDocumentResponse` object.

### By ID

```typescript theme={null}
const post = await client.get("BlogPost", {
  id: "550e8400-e29b-41d4-a716-446655440000",
});
```

### By Slug

```typescript theme={null}
const post = await client.get("BlogPost", {
  slug: "hello-world",
});
```

### With Locale and References

```typescript theme={null}
const post = await client.get("BlogPost", {
  slug: "hello-world",
  locale: "fr",
  resolve: ["author"],
  draft: false,
});
```

### get() Options

| Option    | Type      | Description                                                             |
| --------- | --------- | ----------------------------------------------------------------------- |
| `id`      | string    | Document UUID. Mutually exclusive with `slug` and `path`.               |
| `slug`    | string    | Slug frontmatter value. Mutually exclusive with `id` and `path`.        |
| `path`    | string    | Document path. Mutually exclusive with `id` and `slug`.                 |
| `locale`  | string    | BCP 47 locale tag.                                                      |
| `resolve` | string\[] | Reference field names to resolve (one level deep).                      |
| `draft`   | boolean   | When `true`, return the draft version instead of the published version. |

<Warning>
  When using `slug` or `path`, the query must match exactly one document. If
  multiple documents match (e.g., same slug in different locales without
  specifying `locale`), the SDK throws an `MdcmsClientError` with code
  `AMBIGUOUS_RESULT`.
</Warning>

***

## Querying -- list()

The `list()` method retrieves a paginated list of documents matching the given filters.

```typescript theme={null}
const posts = await client.list("BlogPost", {
  locale: "en",
  published: true,
  sort: "updatedAt",
  order: "desc",
  limit: 10,
});

// Access results
for (const post of posts.data) {
  console.log(post.frontmatter.title);
}

// Access pagination metadata
console.log(posts.pagination.total); // 42
console.log(posts.pagination.hasMore); // true
```

### list() Options

| Option                  | Type      | Description                                        |
| ----------------------- | --------- | -------------------------------------------------- |
| `locale`                | string    | BCP 47 locale tag.                                 |
| `published`             | boolean   | Filter by publication status.                      |
| `isDeleted`             | boolean   | Include soft-deleted documents.                    |
| `hasUnpublishedChanges` | boolean   | Filter to documents with pending draft changes.    |
| `draft`                 | boolean   | Return draft content instead of published content. |
| `resolve`               | string\[] | Reference field names to resolve.                  |
| `limit`                 | number    | Results per page (default 20, max 100).            |
| `offset`                | number    | Number of results to skip.                         |
| `sort`                  | string    | Sort field: `createdAt`, `updatedAt`, or `path`.   |
| `order`                 | string    | Sort direction: `asc` or `desc`.                   |

### Return Type

```typescript theme={null}
{
  data: ContentDocumentResponse[];
  pagination: {
    total: number;
    limit: number;
    offset: number;
    hasMore: boolean;
  };
}
```

***

## Response Type

Both `get()` and `list()` return `ContentDocumentResponse` objects with the following fields:

| Field                   | Type           | Description                                                                                                        |
| ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `documentId`            | string         | Stable UUID identifier.                                                                                            |
| `translationGroupId`    | string         | UUID linking locale variants of the same content.                                                                  |
| `project`               | string         | Project slug.                                                                                                      |
| `environment`           | string         | Environment name.                                                                                                  |
| `path`                  | string         | Filesystem-like path (e.g., `content/blog/hello-world`).                                                           |
| `type`                  | string         | Content type name (e.g., `BlogPost`).                                                                              |
| `locale`                | string         | BCP 47 locale tag, or `__mdcms_default__` for non-localized types.                                                 |
| `format`                | string         | `md` or `mdx`.                                                                                                     |
| `isDeleted`             | boolean        | Whether the document is soft-deleted.                                                                              |
| `hasUnpublishedChanges` | boolean        | Whether the draft differs from the published version.                                                              |
| `version`               | number         | Current version number (incremented on each publish).                                                              |
| `publishedVersion`      | number \| null | Version number of the published snapshot, or `null` if never published.                                            |
| `draftRevision`         | number         | Incrementing counter for draft saves. Used for optimistic concurrency.                                             |
| `frontmatter`           | object         | Structured data matching the content type schema. References are UUIDs unless resolved.                            |
| `body`                  | string         | Markdown or MDX content.                                                                                           |
| `resolveErrors`         | object         | Map of field paths to [resolve error](/api-reference/errors#resolve-errors) objects. Empty when no resolve errors. |
| `createdBy`             | string         | User or API key ID that created the document.                                                                      |
| `createdAt`             | string         | ISO 8601 creation timestamp.                                                                                       |
| `updatedBy`             | string         | User or API key ID that last updated the document.                                                                 |
| `updatedAt`             | string         | ISO 8601 last-update timestamp.                                                                                    |

***

## Error Handling

The SDK provides two error classes for distinguishing between server errors and client-side issues:

```typescript theme={null}
import { MdcmsApiError, MdcmsClientError } from "@mdcms/sdk";

try {
  const post = await client.get("BlogPost", { slug: "missing" });
} catch (error) {
  if (error instanceof MdcmsApiError) {
    // Server returned an error response
    console.error(error.statusCode); // 404
    console.error(error.code); // "NOT_FOUND"
    console.error(error.message); // "Document not found"
    console.error(error.requestId); // "req_abc123"
    console.error(error.timestamp); // "2026-01-15T09:30:00.000Z"
    console.error(error.details); // {}
  }

  if (error instanceof MdcmsClientError) {
    // Client-side error (network, parsing, ambiguity)
    console.error(error.code); // e.g., "NOT_FOUND"
    console.error(error.message); // Human-readable description
  }
}
```

### MdcmsApiError

Thrown when the server returns a non-2xx response. Properties:

| Property     | Type   | Description                                                              |
| ------------ | ------ | ------------------------------------------------------------------------ |
| `statusCode` | number | HTTP status code (e.g., 404, 409, 500).                                  |
| `code`       | string | Machine-readable error code (e.g., `NOT_FOUND`, `SCHEMA_HASH_MISMATCH`). |
| `message`    | string | Human-readable error description.                                        |
| `requestId`  | string | Server request ID for debugging.                                         |
| `timestamp`  | string | ISO 8601 error timestamp.                                                |
| `details`    | object | Additional context specific to the error.                                |

### MdcmsClientError

Thrown for client-side issues that prevent a valid response. Error codes:

| Code                    | Description                                                                                          |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `INVALID_RESPONSE`      | The server returned a response that could not be parsed as valid JSON.                               |
| `NETWORK_ERROR`         | The request failed due to a network issue (DNS failure, connection refused, timeout).                |
| `NOT_FOUND`             | A `get()` call matched zero documents.                                                               |
| `AMBIGUOUS_RESULT`      | A `get()` call by slug or path matched multiple documents. Add a `locale` parameter to disambiguate. |
| `PREVIEW_TOKEN_INVALID` | A preview request did not include a valid MDCMS preview token.                                       |

***

## Next.js Integration

The SDK is designed to work seamlessly with Next.js App Router for both static generation and server-side rendering.

### Client Setup

Create a shared client instance:

```typescript theme={null}
// lib/mdcms.ts
import { createClient } from "@mdcms/sdk";

export const cms = createClient({
  serverUrl: process.env.MDCMS_SERVER_URL!,
  apiKey: process.env.MDCMS_API_KEY!,
  project: process.env.MDCMS_PROJECT!,
  environment: process.env.MDCMS_ENVIRONMENT!,
});
```

### Blog Page Example

```typescript theme={null}
// app/blog/[slug]/page.tsx
import { cms } from "@/lib/mdcms";
import { notFound } from "next/navigation";
import { MdcmsApiError } from "@mdcms/sdk";

export async function generateStaticParams() {
  const posts = await cms.list("BlogPost", { published: true });
  return posts.data.map((post) => ({
    slug: post.frontmatter.slug as string,
  }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  try {
    const post = await cms.get("BlogPost", {
      slug,
      resolve: ["author"],
    });

    return (
      <article>
        <h1>{post.frontmatter.title as string}</h1>
        <div>{post.body}</div>
      </article>
    );
  } catch (error) {
    if (error instanceof MdcmsApiError && error.statusCode === 404) {
      notFound();
    }
    throw error;
  }
}
```

### Blog Index Example

```typescript theme={null}
// app/blog/page.tsx
import { cms } from "@/lib/mdcms";
import Link from "next/link";

export default async function BlogIndex() {
  const posts = await cms.list("BlogPost", {
    published: true,
    sort: "updatedAt",
    order: "desc",
    limit: 20,
  });

  return (
    <div>
      <h1>Blog</h1>
      <ul>
        {posts.data.map((post) => (
          <li key={post.documentId}>
            <Link href={`/blog/${post.frontmatter.slug}`}>
              {post.frontmatter.title as string}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

***

## Draft Preview

Studio mints a short-lived preview token before loading a configured host preview route. The SDK can verify that token and fetch the matching draft document in one call:

```typescript theme={null}
// app/preview/[...path]/route.ts
import { cms } from "@/lib/mdcms";
import { MdcmsClientError } from "@mdcms/sdk";

export const dynamic = "force-dynamic";

export async function GET(request: Request) {
  try {
    const document = await cms.getPreviewDocumentFromRequest(request, {
      secret: process.env.MDCMS_PREVIEW_TOKEN_SECRET!,
      resolve: ["author"],
    });

    return Response.json(document, {
      headers: {
        "Cache-Control": "private, no-store",
      },
    });
  } catch (error) {
    if (
      error instanceof MdcmsClientError &&
      error.code === "PREVIEW_TOKEN_INVALID"
    ) {
      return new Response("Preview unavailable", { status: 401 });
    }

    throw error;
  }
}
```

For lower-level control, verify the request first and then choose your own draft fetch:

```typescript theme={null}
import { verifyMdcmsPreviewRequest } from "@mdcms/sdk";

const preview = await verifyMdcmsPreviewRequest(request, {
  secret: process.env.MDCMS_PREVIEW_TOKEN_SECRET!,
});

if (!preview.ok) {
  return new Response("Preview unavailable", { status: 401 });
}

const document = await cms.get(preview.claims.type, {
  id: preview.claims.documentId,
  locale: preview.claims.locale,
  draft: true,
});
```

When the preview route renders inside Studio's iframe, post
`{ type: "mdcms:live-preview-ready" }` from client-side code after the page is
ready. Studio keeps the pane in a loading state until that message arrives and
shows the fallback link if the route never signals readiness.

<Tip>
  The server-side API key used for private preview routes needs
  `content:read:draft`. Keep that API key and `MDCMS_PREVIEW_TOKEN_SECRET` in
  server-only environment variables, force dynamic rendering, and send
  `Cache-Control: private, no-store` or the framework equivalent.
</Tip>

<Note>
  Some projects intentionally expose drafts on public or separately protected
  preview deployments. MDCMS allows that pattern. If unpublished content must
  stay private, do not treat `?preview=true` alone as authorization; require a
  valid MDCMS preview token, a host session, or another server-side gate before
  fetching with `draft: true`.
</Note>
