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

# Content API

> REST API for reading and updating Weaverse page content, theme settings, and project data from external applications

# Content API

The Weaverse Content API is a REST API that lets you read **and update** your page content, theme settings, and project data from any application — headless frontends, mobile apps, static site generators, AI agents, or custom integrations.

<Note>
  Write endpoints cover page content (update + delete), theme settings, and the
  project name. Content changes use the same cache-invalidation path as a
  Studio save. Project create/delete and store-level settings remain
  Studio-only.
</Note>

## Base URL

```
https://studio.weaverse.io/api/v1/content
```

## Endpoints

| Method | Path | Description |
| - | - | - |
| `GET` | [`/projects`](/docs/content-api/list-projects) | List all projects |
| `GET` | `/projects/:projectId` | Get a single project's metadata |
| `PATCH` | [`/projects/:projectId`](/docs/content-api/update-project) | Update the project name |
| `GET` | [`/projects/:projectId/pages`](/docs/content-api/list-pages) | List page assignments |
| `POST` | [`/projects/:projectId/pages`](/docs/content-api/create-page) | Create a page (`CUSTOM` or a resource-backed template override) |
| `POST` | [`/projects/:projectId/template-assignments/:pageId`](/docs/content-api/assign-template-resources) | Assign many resources to one shared template page |
| `DELETE` | [`/projects/:projectId/pages`](/docs/content-api/delete-pages) | Bulk-delete pages (by ids or handles) |
| `GET` | [`/projects/:projectId/pages/:type/:handle...`](/docs/content-api/get-page) | Get page content |
| `PATCH` | [`/projects/:projectId/pages/:type/:handle...`](/docs/content-api/update-page) | Update page content (merge item data, create items, relink children) |
| `GET` | [`/projects/:projectId/theme-settings`](/docs/content-api/get-theme-settings) | Get theme settings |
| `PATCH` | [`/projects/:projectId/theme-settings`](/docs/content-api/update-theme-settings) | Merge top-level theme settings keys |
| `GET` | [`/projects/:projectId/languages`](/docs/content-api/list-languages) | List configured locales |
| `GET` | `/openapi.json` | OpenAPI 3.1 specification |

The separate public discovery API exposes [`GET /api/public/v1/projects/:projectId/custom-pages`](/docs/content-api/public-custom-pages) for custom-page route metadata. It does not require a Content API key and does not filter assignments by publication state.

All write endpoints also accept `POST` with the same body, for clients and
proxies that strip `PATCH`/`DELETE` bodies. Full request/response schemas live
in [`/openapi.json`](https://studio.weaverse.io/api/v1/content/openapi.json).

### Write semantics

* **Page update** — `PATCH .../pages/:type/:handle...` with
  `{ "items": [{ "id", "data", "type?", "children?" }], "locale?" }`.
  `data` is shallow-merged over the item's existing data (send only changed
  top-level keys; nested objects are replaced wholesale). Unknown ids are
  created only when `type` is supplied. Max 100 items per request.
* **Page create** — `POST .../pages` with
  `{ "type", "handle", "name?", "locale?", "basedOn?", "shopifyResourceId?" }`.
  Creatable types are `CUSTOM`, `PRODUCT`, `COLLECTION`, `PAGE`, `BLOG`,
  `ARTICLE`. `CUSTOM` creates a bespoke page with a blank root item (or clones
  `basedOn`). A resource-backed type creates a per-resource override that clones
  the project's shared default template for that type when one exists (otherwise
  blank), or clones `basedOn`. **This creates one new page per call (a
  per-resource copy).** A duplicate live assignment returns `409` and mutates
  nothing.
* **Assign template resources** — `POST .../template-assignments/:pageId` with
  `{ "assignments": [{ "handle", "shopifyResourceId?", "locale?" }] }`. Assigns
  many resources to **one shared** template page without creating page copies.
  All-or-nothing: a handle already on this page is an idempotent no-op, but if
  any handle belongs to a different live page the whole request returns `409`
  and writes nothing (never repointed).
* **Page delete** — `DELETE .../pages` with `{ "pageIds": [...] }` or
  `{ "handles": [...], "type", "locale?" }`. Accepts up to 500 target values.
  When both selector arrays are present, `pageIds` takes precedence.
* **Theme settings** — `PATCH .../theme-settings` with `{ "theme": { ... } }`.
  Top-level keys are shallow-merged over the project's own theme. For a real
  change, Weaverse attempts a deduplicated snapshot of the merged result, but
  snapshot failure is non-fatal and does not stop the settings write.
* **Project** — `PATCH /projects/:projectId` with `{ "name": "..." }`.
  Only `name` is writable; unknown fields are rejected.

<Tip>
  The page detail endpoint captures the handle as the rest of the path. Use the exact `handle` returned by `/projects/:projectId/pages`; custom page handles may include `/`, such as `pages/gift-shop`.
</Tip>

## Response formats

Page and theme-settings endpoints support two output formats. The default is unchanged — existing integrations are unaffected.

| Format | How to request | Output shape |
| - | - | - |
| `weaverse` (default) | no parameter, or `?format=weaverse` | Flat item tree with `{id, type, data, children}` |
| `portable-text` | `?format=portable-text`, or `Accept: application/portable-text+json` | [Portable Text](https://www.portabletext.org/) array, with rich-text fields converted from HTML strings to structured blocks |

When both the query parameter and the `Accept` header are present, the query parameter wins. See [Get Page › Portable Text format](/docs/content-api/get-page#portable-text-format) for the full mapping rules and a rendering example.

<Tip>
  Portable Text is well-suited for multi-channel rendering (web, native, email, PDF) and for piping content into LLMs, since the structured JSON is far easier to query and reason over than HTML strings.
</Tip>

## Authentication

All endpoints (except `/openapi.json`) require a **Content API key**. Pass it via the `Authorization` header:

```bash theme={null}
curl -H "Authorization: Bearer YOUR_API_KEY" \
  https://studio.weaverse.io/api/v1/content/projects
```

<Warning>
  A `?apiKey=` query parameter fallback exists for development, but tokens in query strings appear in server and CDN logs. **Always use the `Authorization` header in production.**
</Warning>

### Obtaining an API key

Content API keys are provisioned per shop. Generate one from the **Weaverse Dashboard → Settings**.

### Scoping

Each API key is scoped to a single Weaverse shop. Requests can only access projects that belong to that shop. Attempting to access another shop's project returns a `403 Forbidden` error.

## Pagination

List endpoints (`/projects` and `/pages`) use **cursor-based pagination**.

| Parameter | Type | Default | Description |
| - | - | - | - |
| `limit` | integer | `50` | Items per page (max `100`) |
| `after` | string | — | Cursor from a previous response's `nextCursor` |

### Paginated response shape

```json theme={null}
{
  "object": "list",
  "data": [ ... ],
  "nextCursor": "Y2x4MWFiYzIzZGVmNDU2Z2hpag=="
}
```

* `nextCursor` is `null` when there are no more results.
* Pass `nextCursor` as the `after` parameter to fetch the next page.

### Example: iterating through pages

```bash theme={null}
# First page
curl -H "Authorization: Bearer $KEY" \
  "https://studio.weaverse.io/api/v1/content/projects?limit=10"

# Next page (use nextCursor from previous response)
curl -H "Authorization: Bearer $KEY" \
  "https://studio.weaverse.io/api/v1/content/projects?limit=10&after=Y2x4MWFiYzIzZGVmNDU2Z2hpag=="
```

## Error handling

All errors follow a consistent envelope:

```json theme={null}
{
  "object": "error",
  "code": "ERROR_CODE",
  "message": "Human-readable description",
  "status": 400
}
```

### Error codes

| Code | HTTP Status | Description |
| - | - | - |
| `UNAUTHORIZED` | 401 | Missing or invalid API key |
| `FORBIDDEN` | 403 | Valid key, but no access to this resource |
| `PROJECT_NOT_FOUND` | 404 | Project does not exist or was deleted |
| `PAGE_NOT_FOUND` | 404 | Page assignment not found (after locale fallback) |
| `INVALID_PARAMS` | 400 | Malformed request parameters or cursor |
| `INTERNAL_ERROR` | 500 | Unexpected server error |

## Usage tracking & billing

Every authenticated API request counts toward your plan's usage quota. API hits share the same billing pool as storefront page views:

* **1 API request = 1 page view hit**
* Usage is tracked per project and appears in your billing dashboard
* Tracking is non-blocking — it never slows down API responses
* The `/openapi.json` endpoint is public and **not** tracked

<Tip>
  Monitor your API usage in the Weaverse Studio billing dashboard to avoid unexpected overages.
</Tip>

## Quick start

<Steps>
  <Step title="Get your API key">
    Obtain a Content API key from your Weaverse Studio dashboard.
  </Step>

  <Step title="List your projects">
    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      https://studio.weaverse.io/api/v1/content/projects
    ```
  </Step>

  <Step title="Fetch page content">
    ```bash theme={null}
    curl -H "Authorization: Bearer YOUR_API_KEY" \
      "https://studio.weaverse.io/api/v1/content/projects/PROJECT_ID/pages/INDEX/default"
    ```
  </Step>

  <Step title="Render in your app">
    Use the returned `items` array to render page sections in any frontend framework.
  </Step>
</Steps>

## TypeScript example

```typescript theme={null}
const BASE = 'https://studio.weaverse.io/api/v1/content'
const API_KEY = process.env.WEAVERSE_API_KEY

async function fetchPage(projectId: string, type: string, handle: string) {
  const res = await fetch(
    `${BASE}/projects/${projectId}/pages/${type}/${handle}`,
    { headers: { Authorization: ['Bearer', API_KEY].join(' ') } }
  )

  if (!res.ok) {
    const error = await res.json()
    throw new Error(`[${error.code}] ${error.message}`)
  }

  return res.json()
}

// Usage
const page = await fetchPage('clx1abc23def456ghij', 'PRODUCT', 'blue-shirt')
const customPage = await fetchPage('clx1abc23def456ghij', 'CUSTOM', 'pages/gift-shop')
console.log(page.items, customPage.items) // Arrays of section components
```

## Next steps

<CardGroup cols={2}>
  <Card title="List Projects" icon="folder" href="/docs/content-api/list-projects">
    Retrieve all projects in your shop
  </Card>

  <Card title="List Pages" icon="file-lines" href="/docs/content-api/list-pages">
    Browse page assignments in a project
  </Card>

  <Card title="Get Page Content" icon="cube" href="/docs/content-api/get-page">
    Fetch full page data with component items
  </Card>

  <Card title="Get Theme Settings" icon="palette" href="/docs/content-api/get-theme-settings">
    Retrieve theme design tokens and configuration
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.