> ## 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.

# Scheduled and automated Pages

> Keep a report or dashboard URL fresh from a cron job, with no conversation involved.

A Page does not need a chat. With a bot API key that has **Pages write** switched on, any scheduler — n8n, Make, Zapier, a cron job, GitHub Actions — can create and refresh Pages directly. This is the recipe.

## The idea

Pick one slug per report and `PUT` to it on every run:

```text theme={null}
PUT https://app.my-aichatbot.com/api/v1/pages/by-slug/sales-report
Authorization: Bearer mac_live_…
```

* The first run **creates** the Page (`201`).
* Every later run **adds a version** (`200`), and the same URL now shows it.
* Unchanged HTML is skipped (`"deduplicated": true`), so a run with nothing new costs no storage and no version.
* Renaming the slug in the app does not break the job. The old slug still reaches the same Page.

You never have to store a page id.

## Step by step

<Steps>
  <Step title="Create a key with Pages write">
    In your bot's settings, open **API keys**, create a key, and turn on **Pages write**. The key is shown once. Keep it on your server.
  </Step>

  <Step title="Render the HTML">
    Build a complete HTML document. Give it a `<title>`: it becomes the Page title when you do not send one.
  </Step>

  <Step title="PUT it to the slug">
    <CodeGroup>
      ```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 "$(jq -n --rawfile html report.html \
          '{ title: "Sales report", visibility: "private", content: $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()
      ```
    </CodeGroup>
  </Step>

  <Step title="Share the URL once">
    The response carries `url`. Pin it in a channel, put it on a dashboard, or post a [`page_preview`](/block-kit/blocks/page-preview) card. It stays valid for the life of the Page.
  </Step>
</Steps>

## One Page per dataset

Slugs are the unit. `sales-report`, `support-queue` and `uptime-eu` are three Pages, each refreshed by its own job, or by one job in a loop. Your plan caps how many Pages a workspace can hold, so reuse slugs rather than minting dated ones like `sales-2026-09-21`. The version history already keeps every run.

## Send only what the job owns

Whatever you send is applied on every run, and whatever you leave out is left alone. Send `title` and `visibility` if the job owns them. Leave them out if someone manages them in the app, otherwise the next run overwrites their change.

## How often?

| Cadence                | Pattern                                                                                                                                                          |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Daily or hourly report | `PUT` the rendered HTML each run. The version history doubles as an archive, pruned to your plan's limit.                                                        |
| Every few minutes      | Same, if the numbers actually change. Identical HTML is skipped, but every changed run is a version.                                                             |
| Live                   | Publish a static Page once whose script fetches JSON from your own endpoint (it must allow cross-origin requests). Your job then updates the data, not the Page. |

## Posting it to a chat

The job can also tell people. `POST` a [`page_preview`](/block-kit/blocks/page-preview) block to any of the bot's conversations with the same API key, using `page.id`, `page.title` and `url` from the upsert response:

```json theme={null}
{
  "mid": "sales-report-card",
  "blocks": [
    { "type": "text", "text": "The sales report just refreshed." },
    {
      "type": "page_preview",
      "page_id": "9b3f11c2-…",
      "url": "https://acme.my-aichatbot-space.com/sales-report",
      "title": "Sales report"
    }
  ]
}
```

With a fixed `mid`, the card updates in place instead of stacking up. See [Post a message (API key)](/api-reference/post-message).

## No-code recipes

<CardGroup cols={3}>
  <Card title="n8n" icon="workflow" href="/integrations/n8n#publish-a-page-on-a-schedule">
    Schedule Trigger + HTTP Request.
  </Card>

  <Card title="Make" icon="boxes" href="/integrations/make#publish-a-page-on-a-schedule">
    A scheduled scenario + HTTP module.
  </Card>

  <Card title="Zapier" icon="zap" href="/integrations/zapier#publish-a-page-on-a-schedule">
    Schedule by Zapier + a custom request.
  </Card>
</CardGroup>

## Errors you might see

| Status | Code                   | Meaning                                                                           |
| ------ | ---------------------- | --------------------------------------------------------------------------------- |
| `403`  | `PAGES_WRITE_DISABLED` | Turn on **Pages write** for the key.                                              |
| `403`  | `FORBIDDEN_CHATBOT`    | Another bot's Page holds that slug. Pick a different slug, or use that bot's key. |
| `402`  | `PAGE_LIMIT_REACHED`   | Reuse a slug or delete an old Page.                                               |
| `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.                                                 |
