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

# Create or update by slug

> One call a scheduled job can repeat forever: creates the Page on the first run, adds a version on every run after.

The automation-friendly write. If a Page in your workspace holds this slug (or held it before a rename), the call adds a version to it. Otherwise it creates the Page under this slug. You never have to store a page id.

Needs a key with **Pages write** turned on. See [Scheduled and automated Pages](/pages/scheduled-pages) for the full recipe.

## Authorization

<ParamField header="Authorization" type="string" required>
  `Bearer mac_live_…`
</ParamField>

## Path parameters

<ParamField path="slug" type="string" required>
  1 to 63 lowercase letters, digits and dashes. Anything else is `422 VALIDATION_FAILED`.
</ParamField>

## Body

<ParamField body="content" type="string" required>
  The full HTML document. Up to your plan's per-version cap (512 KB on Free).
</ParamField>

<ParamField body="title" type="string">
  Up to 150 characters. On create, without it we use the HTML `<title>`, then "Untitled page". On update it renames the Page.
</ParamField>

<ParamField body="description" type="string | null">
  Plain text, up to 300 characters. `null` clears it.
</ParamField>

<ParamField body="visibility" type="string">
  `private` or `public`. Defaults to `private` on create. On update it changes the Page's visibility.
</ParamField>

<ParamField body="change_note" type="string">
  Up to 500 characters, shown next to the version in the app.
</ParamField>

<ParamField body="conversation_id" type="string">
  Link the Page to a conversation. Only used on create.
</ParamField>

<Note>
  Every run applies the fields it sends and leaves the rest alone, so leave out `title` or `visibility` if someone manages them in the app. Otherwise the next run overwrites their change.
</Note>

## Response

`201 Created` when this call created the Page, `200 OK` when it updated one.

<ResponseField name="ok" type="boolean">
  `true` on success.
</ResponseField>

<ResponseField name="created" type="boolean">
  `true` when this call created the Page.
</ResponseField>

<ResponseField name="page" type="object">
  The Page. Same fields as [Get a page](/api-reference/pages/get-page).
</ResponseField>

<ResponseField name="version" type="integer">
  The version this call produced, or the current one when `deduplicated`.
</ResponseField>

<ResponseField name="url" type="string">
  The Page's link: the members-only door for `private`, the Page itself for `public`.
</ResponseField>

<ResponseField name="version_url" type="string">
  The permanent `/v/{n}` address of this version. For a private Page it sends members through the sign-in door first.
</ResponseField>

<ResponseField name="content_sha256" type="string">
  SHA-256 of the stored content.
</ResponseField>

<ResponseField name="deduplicated" type="boolean">
  `true` when the content was byte-for-byte identical to the current version, so nothing new was stored.
</ResponseField>

<RequestExample>
  ```bash curl theme={null}
  curl -X PUT "https://app.my-aichatbot.com/api/v1/pages/by-slug/sales-report" \
    -H "Authorization: Bearer $MAC_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "title": "Sales report",
      "visibility": "private",
      "content": "<!doctype html><html><head><title>Sales</title></head><body><h1>Sales</h1></body></html>",
      "change_note": "hourly"
    }'
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    'https://app.my-aichatbot.com/api/v1/pages/by-slug/sales-report',
    {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${process.env.MAC_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        title: 'Sales report',
        visibility: 'private',
        content: html,
        change_note: 'hourly'
      })
    }
  )

  const { created, version, url, deduplicated } = await res.json()
  ```

  ```python Python theme={null}
  import os, requests

  res = requests.put(
      "https://app.my-aichatbot.com/api/v1/pages/by-slug/sales-report",
      headers={"Authorization": f"Bearer {os.environ['MAC_KEY']}"},
      json={
          "title": "Sales report",
          "visibility": "private",
          "content": html,
          "change_note": "hourly",
      },
  )

  body = res.json()
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "ok": true,
    "created": true,
    "page": {
      "id": "9b3f11c2-…",
      "slug": "sales-report",
      "title": "Sales report",
      "description": null,
      "visibility": "private",
      "chatbot_id": "c1a2…",
      "conversation_id": null,
      "current_version": 1,
      "version_count": 1,
      "total_bytes": 2048,
      "disabled_reason": null,
      "created_at": "2026-09-21T03:00:00.000Z",
      "updated_at": "2026-09-21T03:00:00.000Z",
      "url": "https://app.my-aichatbot.com/pages/view/acme/sales-report"
    },
    "version": 1,
    "url": "https://app.my-aichatbot.com/pages/view/acme/sales-report",
    "version_url": "https://acme.my-aichatbot-space.com/sales-report/v/1",
    "content_sha256": "3a7bd3e2…",
    "deduplicated": false
  }
  ```

  ```json 200 Nothing changed theme={null}
  {
    "ok": true,
    "created": false,
    "page": { "id": "9b3f11c2-…", "slug": "sales-report", "current_version": 4 },
    "version": 4,
    "url": "https://app.my-aichatbot.com/pages/view/acme/sales-report",
    "version_url": "https://acme.my-aichatbot-space.com/sales-report/v/4",
    "content_sha256": "3a7bd3e2…",
    "deduplicated": true
  }
  ```

  ```json 403 Another bot's Page theme={null}
  {
    "ok": false,
    "error": {
      "code": "FORBIDDEN_CHATBOT",
      "message": "This key's chatbot does not own that page (writes are scoped to the key's chatbot)."
    }
  }
  ```
</ResponseExample>

## Errors

| Status | Code                     | Meaning                                                                      |
| ------ | ------------------------ | ---------------------------------------------------------------------------- |
| `401`  | `INVALID_API_KEY`        | Missing, broken, or revoked.                                                 |
| `402`  | `PAGE_LIMIT_REACHED`     | Creating would exceed your plan's Page count. Reuse a slug or delete a Page. |
| `402`  | `STORAGE_QUOTA_EXCEEDED` | Workspace storage is full.                                                   |
| `403`  | `PAGES_WRITE_DISABLED`   | Turn on **Pages write** for this key.                                        |
| `403`  | `FORBIDDEN_CHATBOT`      | Another bot's Page holds this slug.                                          |
| `403`  | `PAGE_DISABLED`          | The Page was disabled by an admin. Writes are rejected.                      |
| `409`  | `SLUG_TAKEN`             | The slug belonged to a deleted Page and is retired. Pick another.            |
| `413`  | `CONTENT_TOO_LARGE`      | Over your plan's per-version cap. `limit_bytes` says how much.               |
| `422`  | `VALIDATION_FAILED`      | Bad slug or body.                                                            |
| `429`  | `RATE_LIMITED`           | Over 120 writes per minute for this key. Wait for `retry_after_seconds`.     |
| `500`  | `PERSIST_FAILED`         | We could not store the Page. Safe to retry.                                  |
