# Sites · Management API

> Management API reference for sites: GET /sites, POST /sites, GET /sites/{id}, GET /sites/{id}/structure, ….

> Markdown version of https://kanorio.com/en/developers/management-api/sites. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp

Base URL: `https://app.kanorio.com/api/v1` · [Errors](https://kanorio.com/en/developers/basics/errors) · [Pagination](https://kanorio.com/en/developers/basics/pagination)

### List sites

`GET /sites`

- Stability: beta
- Permission: `console:view`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_list`

List the websites the token owner is a member of, restricted to the websites the token was granted for, most recently updated first. Returns ids you need for every other site-scoped call. Errors: FORBIDDEN, INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Maximum number of items to return, 1–100. Defaults to 20. |
| `cursor` | query | string | no | Opaque cursor from the previous page's nextCursor. Omit for the first page. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].name` | string | yes | — |
| `data[].role` | string \| null | yes | The token owner's role on this website (OWNER/ADMIN/MEMBER/VIEWER). |
| `data[].plan` | string | yes | — |
| `data[].locale` | string \| null | yes | Content language of the site (e.g. zh-Hant, en-US). |
| `data[].status` | string | yes | — |
| `data[].isPublished` | boolean | yes | — |
| `data[].subdomain` | string \| null | yes | Assigned on first publish; null until then. |
| `data[].suggestedSubdomain` | string \| null | yes | Suggested slug to use when publishing for the first time. |
| `data[].publicUrl` | string \| null | yes | https URL on kanorio.com when published. |
| `data[].primaryCustomDomain` | object \| null | yes | — |
| `data[].primaryCustomDomain.hostname` | string | yes | — |
| `data[].primaryCustomDomain.status` | string | yes | — |
| `data[].lastPublishedAt` | string \| null | yes | — |
| `data[].hasUnpublishedChanges` | boolean | yes | True when pages or site-wide layout/theme have unpublished draft edits (publish with site.publish). Blog posts publish individually via posts.publish. |
| `data[].postsWithDraftChanges` | integer | no | Published blog posts that have unpublished draft edits (call posts.publish on each to publish them). |
| `data[].draftPosts` | integer | no | Blog posts currently in DRAFT status. |
| `data[].createdAt` | string | yes | — |
| `data[].updatedAt` | string | yes | — |
| `hasMore` | boolean | yes | True when another page exists; pass nextCursor to fetch it. |
| `nextCursor` | string \| null | yes | Cursor for the next page, or null on the last page. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `INVALID_CURSOR` | 400 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "data": [
    {
      "id": "ws_example123",
      "name": "Sunny Bakery",
      "role": "OWNER",
      "plan": "PRO",
      "locale": "zh-Hant",
      "status": "ACTIVE",
      "isPublished": true,
      "subdomain": "sunny-bakery",
      "suggestedSubdomain": "sunny-bakery",
      "publicUrl": "https://sunny-bakery.kanorio.com",
      "primaryCustomDomain": {
        "hostname": "www.sunnybakery.example",
        "status": "ACTIVE"
      },
      "lastPublishedAt": "2026-10-01T02:30:00.000Z",
      "hasUnpublishedChanges": true,
      "createdAt": "2026-08-12T03:20:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Create a site

`POST /sites`

- Stability: beta
- Permission: `console:view`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_create`

Create a new website owned by the token owner. Requires a token granted for all websites (not restricted to specific websiteIds). mode=blank: creates the skeleton immediately (empty home page, hidden legal pages, a default header and footer in globalSections, owner membership) and returns websiteId. Then follow sections.describe -> conventions.brandFirst: set the theme (site.update_settings), define buttons (globals.update_site_settings), create pages, fill them with pages.replace_sections, rebuild the header navigation (globals.replace_header) and publish. mode=brief: queues the AI site builder with a brand brief and returns operationId; poll operations.get every 5-10 seconds until status is SUCCEEDED (result.websiteId is set) or FAILED. This consumes the owner's AI credits and daily site-creation quota. Errors: TOKEN_WEBSITE_SCOPED, VALIDATION, DAILY_LIMIT_REACHED (brief).

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `mode` | "blank" \| "brief" | yes | — |
| `websiteId` | string \| null | yes | Set immediately for blank; for brief it appears on operations.get once the job succeeds. |
| `operationId` | string \| null | yes | Poll with operations.get (brief mode only). |
| `jobId` | string \| null | yes | Deprecated alias of the underlying job id; use operationId with operations.get. |
| `status` | string \| null | yes | Job status for brief mode. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `TOKEN_WEBSITE_SCOPED` | 403 | — |
| `VALIDATION` | 400 | — |
| `DAILY_LIMIT_REACHED` | 429 | brief |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "mode": "blank",
  "websiteId": "ws_example123",
  "operationId": null,
  "jobId": null,
  "status": null
}
```

### Retrieve a site

`GET /sites/{id}`

- Stability: beta
- Permission: `console:view`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_get`

Get one website's summary: plan, publish state, subdomain, primary custom domain and whether there are unpublished changes. Errors: FORBIDDEN (also when the website does not exist), WEBSITE_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `name` | string | yes | — |
| `role` | string \| null | yes | The token owner's role on this website (OWNER/ADMIN/MEMBER/VIEWER). |
| `plan` | string | yes | — |
| `locale` | string \| null | yes | Content language of the site (e.g. zh-Hant, en-US). |
| `status` | string | yes | — |
| `isPublished` | boolean | yes | — |
| `subdomain` | string \| null | yes | Assigned on first publish; null until then. |
| `suggestedSubdomain` | string \| null | yes | Suggested slug to use when publishing for the first time. |
| `publicUrl` | string \| null | yes | https URL on kanorio.com when published. |
| `primaryCustomDomain` | object \| null | yes | — |
| `primaryCustomDomain.hostname` | string | yes | — |
| `primaryCustomDomain.status` | string | yes | — |
| `lastPublishedAt` | string \| null | yes | — |
| `hasUnpublishedChanges` | boolean | yes | True when pages or site-wide layout/theme have unpublished draft edits (publish with site.publish). Blog posts publish individually via posts.publish. |
| `postsWithDraftChanges` | integer | no | Published blog posts that have unpublished draft edits (call posts.publish on each to publish them). |
| `draftPosts` | integer | no | Blog posts currently in DRAFT status. |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | also when the website does not exist |
| `WEBSITE_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "id": "ws_example123",
  "name": "Sunny Bakery",
  "role": "OWNER",
  "plan": "PRO",
  "locale": "zh-Hant",
  "status": "ACTIVE",
  "isPublished": true,
  "subdomain": "sunny-bakery",
  "suggestedSubdomain": "sunny-bakery",
  "publicUrl": "https://sunny-bakery.kanorio.com",
  "primaryCustomDomain": {
    "hostname": "www.sunnybakery.example",
    "status": "ACTIVE"
  },
  "lastPublishedAt": "2026-10-01T02:30:00.000Z",
  "hasUnpublishedChanges": true,
  "createdAt": "2026-08-12T03:20:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z"
}
```

### Site structure

`GET /sites/{id}/structure`

- Stability: beta
- Permission: `editor:enter`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_structure`

Return the visual sitemap tree of a website: top-level pages with nested subpages (up to 3 levels), real page status (published / changed / draft / hidden), the ordered list of section types on each page, and summary counts for Kanorio-managed Store and Blog pages. Errors: FORBIDDEN (also when the website does not exist), WEBSITE_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | — |
| `totalGeneralPages` | integer | yes | — |
| `maxDepth` | integer | yes | — |
| `rootPages` | object[] | yes | — |
| `rootPages[].id` | string | yes | — |
| `rootPages[].slug` | string | yes | — |
| `rootPages[].title` | string | yes | — |
| `rootPages[].pageType` | string | yes | — |
| `rootPages[].isHomePage` | boolean | yes | — |
| `rootPages[].isVisible` | boolean | yes | — |
| `rootPages[].isDirectory` | boolean | yes | — |
| `rootPages[].status` | "published" \| "changed" \| "draft" \| "hidden" | yes | — |
| `rootPages[].path` | string[] | yes | — |
| `rootPages[].parentSlug` | string \| null | yes | — |
| `rootPages[].depth` | integer | yes | — |
| `rootPages[].sections` | string[] | yes | Ordered list of content section types on this page (excluding global header and footer). |
| `rootPages[].hasGlobalChrome` | boolean | yes | — |
| `rootPages[].descendantCount` | integer | yes | — |
| `rootPages[].children` | any[] | yes | — |
| `systemGroups` | object | yes | — |
| `systemGroups.shop` | object | yes | — |
| `systemGroups.shop.basePath` | string | yes | — |
| `systemGroups.shop.productCount` | integer | yes | Active non-template products in the store (excludes sample products). |
| `systemGroups.shop.productCategoryCount` | integer | yes | — |
| `systemGroups.blog` | object | yes | — |
| `systemGroups.blog.basePath` | string | yes | — |
| `systemGroups.blog.publishedPostCount` | integer | yes | — |
| `systemGroups.blog.blogCategoryCount` | integer | yes | — |
| `systemGroups.blog.blogTagCount` | integer | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | also when the website does not exist |
| `WEBSITE_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/structure" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "websiteId": "ws_example123",
  "totalGeneralPages": 2,
  "maxDepth": 1,
  "rootPages": [
    {
      "id": "pg_example456",
      "slug": "/",
      "title": "Home",
      "pageType": "generic",
      "isHomePage": true,
      "isVisible": true,
      "isDirectory": false,
      "status": "published",
      "path": [],
      "parentSlug": null,
      "depth": 0,
      "sections": [
        "page-main-visual",
        "faq"
      ],
      "hasGlobalChrome": true,
      "descendantCount": 0,
      "children": []
    },
    {
      "id": "pg_example789",
      "slug": "/about",
      "title": "About Us",
      "pageType": "generic",
      "isHomePage": false,
      "isVisible": true,
      "isDirectory": false,
      "status": "published",
      "path": [
        "about"
      ],
      "parentSlug": null,
      "depth": 1,
      "sections": [
        "image-text-split"
      ],
      "hasGlobalChrome": true,
      "descendantCount": 0,
      "children": []
    }
  ],
  "systemGroups": {
    "shop": {
      "basePath": "/shop",
      "productCount": 12,
      "productCategoryCount": 3
    },
    "blog": {
      "basePath": "/blog",
      "publishedPostCount": 27,
      "blogCategoryCount": 5,
      "blogTagCount": 14
    }
  }
}
```

### Site usage

`GET /sites/{id}/usage`

- Stability: beta
- Permission: `console:view`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_usage`

Get the website's plan tier, storage quota & usage (usedBytes, limitBytes, remainingBytes), and content counts (general pages, blog posts, active products, custom domains) so you can check limits before uploading or creating resources. Errors: FORBIDDEN.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | Website id. |
| `plan` | string | yes | Current plan tier (FREE, PRO, BUSINESS). |
| `storage` | object | yes | — |
| `storage.usedBytes` | number | yes | Bytes counted against the storage limit. |
| `storage.limitBytes` | number | yes | Plan storage limit in bytes (including add-on levels). |
| `storage.remainingBytes` | number | yes | Remaining storage in bytes. |
| `storage.scope` | "website" \| "account" | yes | Whether the storage pool is per-website or shared across the owner's FREE sites. |
| `counts` | object | yes | — |
| `counts.generalPages` | integer | yes | Total non-product pages. |
| `counts.visiblePages` | integer | yes | Visible non-product pages. |
| `counts.publishedPosts` | integer | yes | Published blog posts. |
| `counts.draftPosts` | integer | yes | Draft blog posts. |
| `counts.activeProducts` | integer | yes | Active non-template products. |
| `counts.customDomains` | integer | yes | Bound custom domains. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/usage" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "websiteId": "ws_example123",
  "plan": "PRO",
  "storage": {
    "usedBytes": 482133,
    "limitBytes": 10737418240,
    "remainingBytes": 10736936107,
    "scope": "website"
  },
  "counts": {
    "generalPages": 5,
    "visiblePages": 4,
    "publishedPosts": 12,
    "draftPosts": 1,
    "activeProducts": 8,
    "customDomains": 1
  }
}
```

### Diagnose a site

`GET /sites/{id}/diagnose`

- Stability: beta
- Permission: `console:view`
- Rate limit: default (120/minute per token)
- MCP tool: `sites_diagnose`

Run a one-call health and readiness diagnosis on the website: checks unpublished page/theme changes, unpublished blog post edits, missing media files, pending custom domain DNS, search engine indexing status, empty SEO meta descriptions, and unedited sample products. Each finding includes severity and fixWith (the capability or dashboard path to resolve it). Errors: FORBIDDEN.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `websiteId` | string | yes | Website id. |
| `overallStatus` | "healthy" \| "attention" \| "action_required" | yes | Summary health status. |
| `findings` | object[] | yes | — |
| `findings[].id` | string | yes | Finding identifier. |
| `findings[].severity` | "info" \| "warn" \| "error" | yes | Severity level. |
| `findings[].summary` | string | yes | Human-readable explanation of the finding. |
| `findings[].fixWith` | string | yes | Capability name or dashboard route to resolve this finding. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/diagnose" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "websiteId": "ws_example123",
  "overallStatus": "attention",
  "findings": [
    {
      "id": "unpublished_page_changes",
      "severity": "info",
      "summary": "There are unpublished draft changes on pages or site-wide layout.",
      "fixWith": "site.publish"
    }
  ]
}
```
