# Page data

Every DocPin page has a **data store**: one JSON object, up to 1 MB, kept separately from the page's HTML. The page reads it in the browser. You change it from the API, from an AI app over MCP, or in the DocPin web app, and the page shows the new values without being republished.

Use it for anything that changes more often than the page itself: dashboards, status boards, price lists, scores, schedules, "last updated" banners.

## How it fits together

```
 your script / cron / CI ──PATCH /v1/pages/{id}/data──┐
 Claude or ChatGPT ───────set_page_data (MCP)─────────┤
 DocPin web app ──────────Data tab────────────────────┤
                                                      ▼
                                          page data (JSON, ≤ 1 MB)
                                                      │
                     docpin.data() / docpin.onData() ◀┘  (in the published page)
```

- Updating data **doesn't create a version**, and the page's version history isn't touched.
- Data belongs to the page, not to a version: publishing a new version, or restoring an old one, keeps the current data.
- Deleting the page deletes its data.
- Available on Free and Pro.

## Read it in the page

DocPin adds a small script to every published page, which provides a global `docpin` object. You don't include anything yourself.

### `docpin.data()`

Returns a promise for the page's data object (`{}` if nothing has been set).

```html
<div id="revenue">…</div>
<script>
  docpin.data().then((data) => {
    document.getElementById("revenue").textContent =
      data.revenue == null ? "No data yet" : "$" + data.revenue.toLocaleString();
  });
</script>
```

After it resolves, `docpin.updatedAt` holds when the data last changed (ISO 8601), or `null`.

### `docpin.onData(callback, { every })`

Reads the data now, then re-checks every `every` seconds (default 30, minimum 10) and calls `callback(data)` whenever it changed. Checks pause while the tab is hidden. It returns a function that stops checking.

```js
const stop = docpin.onData((data) => render(data), { every: 60 });
// later, if you need to: stop();
```

Use `onData` for anything left open on a screen, like a TV dashboard: new data appears without anyone reloading.

### Writing pages that use data

- **Render from data, not from hard-coded numbers.** Keep the layout in the HTML and the values in the data store, so updates never need a republish.
- **Always handle empty data.** A new page has `{}`. Show a clear empty state ("No data yet") rather than `undefined` or `NaN`.
- **Treat every field as optional.** Someone may update one key and not another, so check each value before using it.
- **Keep the shape flat and named.** Prefer `{ "revenue": 128400, "regions": [{ "name": "NZ", "value": 40 }] }` over deeply nested or positional data.
- **Include your own timestamp if it matters to readers**, e.g. `"asOf": "2026-11-06T09:00:00Z"`. `docpin.updatedAt` is when the data was stored, which may differ from when the numbers were measured.

## Update it from code: the REST API

Base URL: `https://api.docpin.app`. Authenticate with a workspace API key: `Authorization: Bearer dp_…`. Workspace owners create keys in the DocPin app under **Connections → API keys**. You need the page's **id**, which `GET /v1/pages` lists and every publish response includes as `doc.id`.

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/pages/{id}/data` | Returns `{ "data": {…}, "updatedAt": "…" \| null }` |
| `PATCH` | `/v1/pages/{id}/data` | **Merges** the top-level keys you send. A `null` value removes that key. |
| `PUT` | `/v1/pages/{id}/data` | **Replaces** all of the page's data with the object you send. |

The request body is the JSON object itself, not wrapped. Both writes return the full stored data and its new `updatedAt`.

```bash
curl -X PATCH https://api.docpin.app/v1/pages/$PAGE_ID/data \
  -H "Authorization: Bearer $DOCPIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"revenue": 128400, "asOf": "2026-11-06T09:00:00Z", "stale": null}'
```

```json
{ "data": { "revenue": 128400, "asOf": "2026-11-06T09:00:00Z" }, "updatedAt": "2026-11-06T09:00:02.114Z" }
```

**Prefer `PATCH`.** It only touches the keys you send, so two jobs that each own different keys (say, sales and support numbers) can't overwrite each other. Use `PUT` when one job owns the whole object.

### On a schedule

Any scheduler that can make an HTTPS request works: cron, GitHub Actions, a serverless function, Zapier or Make. For example, a GitHub Actions workflow:

```yaml
on:
  schedule: [{ cron: "*/15 * * * *" }]
jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - run: |
          REVENUE=$(curl -s https://example.com/internal/revenue)   # your source
          curl -fsS -X PATCH https://api.docpin.app/v1/pages/${{ vars.PAGE_ID }}/data \
            -H "Authorization: Bearer ${{ secrets.DOCPIN_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d "{\"revenue\": $REVENUE, \"asOf\": \"$(date -u +%FT%TZ)\"}"
```

Keep the API key in your scheduler's secrets. It can publish and change every page in the workspace.

## Update it from an AI app: MCP

With DocPin connected to Claude or ChatGPT (see [MCP](/docs/mcp.md)), two tools work with page data:

| Tool | Arguments | What it does |
| --- | --- | --- |
| `get_page_data` | `slug` (and `board` if in one), or `id` | Returns the page's current data and when it changed. |
| `set_page_data` | the page, `data` (object), optional `replace: true` | Merges `data` in (a `null` removes a key), or replaces everything with `replace: true`. |

People can ask in plain language: "update the sales dashboard with these numbers", "clear the old regions from the dashboard data". An AI app should call `get_page_data` before replacing data, so it doesn't drop keys it didn't know about.

Scheduled AI tasks (for example a daily Claude task that pulls figures from a connected tool) can call `set_page_data` the same way.

## Edit it by hand

In the DocPin web app, open the page and choose the **Data** tab. Edit the JSON and choose **Save data**. Only the keys you changed are sent, so a scheduled update made while you were editing isn't undone.

## Limits

| Limit | Value |
| --- | --- |
| Size | 1 MB per page, measured as JSON text. Larger writes get `413`. |
| Shape | The top level must be a JSON object. Values can be any JSON. |
| Keys | 1 to 200 characters. |
| Writes | About 30 a minute per workspace. Over that you get `429`; wait a minute. |
| Checks from pages | `docpin.onData` checks at most every 10 seconds per open page. |

## Who can see it

**Page data is exactly as visible as the page.** It's served from the page's own address:
- On a public page, anyone with the link can read the data.
- On a password page, only people who entered the password can read it.
- On a people-only page, only the people allowed in can read it.

When you change a page's password or people list, links that people opened before the change stop working for the page and its data until they open the page again.

Version previews never show the current data.

**Never put secrets in page data**: no API keys, passwords, tokens or personal information you wouldn't put on the page itself. If a page needs data from a private source, have your server or scheduled job fetch it and push only the results.

## Errors

Errors come back as `{ "error": "Plain-English message" }` with these statuses:

| Status | Meaning |
| --- | --- |
| `400` | The body isn't a JSON object, or a key is empty or too long. |
| `401` | Missing or invalid API key, or the person who created the key left the workspace. |
| `403` | DocPin took the page down. |
| `404` | No such page in this workspace (check the id). |
| `409` | The data changed several times while saving. Retry. |
| `413` | Over 1 MB. |
| `429` | Too many writes. Wait a minute. |

## Related

- [Live dashboard recipe](/docs/recipes/live-dashboard.md): a complete page and update script to copy.
- [How pages run](/docs/page-runtime.md): links, URL state and browser storage.
- [REST API](/docs/api.md) and [MCP](/docs/mcp.md) references.
