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

# Pages

> Create, list, retrieve, update, and delete pages.

All endpoints require an `Authorization: Bearer sm_…` header and live under `https://useshareable.com/api/v1`.

## The page object

<ResponseField name="id" type="string">Unique page ID.</ResponseField>
<ResponseField name="slug" type="string">URL slug. The page is served at `/p/{slug}`.</ResponseField>
<ResponseField name="url" type="string">Full public URL.</ResponseField>
<ResponseField name="title" type="string | null">Display title.</ResponseField>
<ResponseField name="access" type="string">`link`, `people`, `private`, `password`, or `email_gate`. See [access levels](/publishing-and-access).</ResponseField>
<ResponseField name="perm" type="string">Your permission on the page (`owner` for pages you created via the API).</ResponseField>
<ResponseField name="allowed_emails" type="string[]">Invited emails (when `access` is `people`).</ResponseField>
<ResponseField name="people" type="object[]">The invite list as `{ email, role }` objects (role is `view`). Kept in sync with `allowed_emails`.</ResponseField>
<ResponseField name="password" type="string">The page password, for you to share (only on single-page responses — create, get, update, publish — when `access` is `password`; omitted from the list).</ResponseField>
<ResponseField name="accent" type="string">The page's accent color (e.g. `indigo`).</ResponseField>
<ResponseField name="source" type="string">How the page was created (e.g. `mcp`, `upload`).</ResponseField>
<ResponseField name="indexed" type="boolean">Whether search engines may index the page. Default `false`.</ResponseField>
<ResponseField name="kind" type="string">`single` for a one-file page, or `bundle` for a multi-page deck.</ResponseField>
<ResponseField name="entry_path" type="string | null">For a `bundle`, the file served at the deck root (e.g. `index.html`).</ResponseField>
<ResponseField name="file_count" type="number">Number of HTML files (always `1` for a `single` page).</ResponseField>
<ResponseField name="is_published" type="boolean">Whether a published version is live.</ResponseField>
<ResponseField name="has_unpublished_changes" type="boolean">Whether the draft differs from the published version.</ResponseField>
<ResponseField name="published_at" type="string | null">ISO timestamp of last publish.</ResponseField>

<ResponseField name="created_at" type="string" />

<ResponseField name="updated_at" type="string" />

<ResponseField name="views" type="number">Total recorded views.</ResponseField>
<ResponseField name="expires_at" type="string | null">If set, the link stops working after this ISO 8601 datetime.</ResponseField>
<ResponseField name="max_views" type="number | null">If set, the link closes after this many views.</ResponseField>
<ResponseField name="download_disabled" type="boolean">View-only: for a PDF/image, the download button is hidden and the file is shown inline only.</ResponseField>
<ResponseField name="notify_on_view" type="boolean">Whether the owner is emailed each time the page is opened.</ResponseField>
<ResponseField name="allow_comments" type="boolean">Whether viewers can comment on the page.</ResponseField>
<ResponseField name="comments_require_email" type="boolean">Whether commenters must confirm an email first (`false` = anonymous).</ResponseField>
<ResponseField name="collect_approvals" type="boolean">**Pro.** Collect email-verified client sign-off — designated approvers Approve / Request changes on the page. Set `approver_emails` too.</ResponseField>
<ResponseField name="approver_emails" type="string[]">The designated approver emails (used with `collect_approvals`).</ResponseField>
<ResponseField name="approvals_visibility" type="string">`owner` (default — decisions private to you) or `shared` (approvers see each other's decisions).</ResponseField>
<ResponseField name="collect_votes" type="boolean">Add a one-question poll to the page. Set `poll_question` + `poll_options` too.</ResponseField>
<ResponseField name="poll_question" type="string | null">The poll question.</ResponseField>
<ResponseField name="poll_options" type="string[]">The poll options (2–12).</ResponseField>
<ResponseField name="poll_require_email" type="boolean">Poll identity: `true` = verified email (one per person); `false` = anonymous (one per device).</ResponseField>
<ResponseField name="poll_results" type="string">Who sees results: `public` (default), `vote` (after voting), or `owner` (only you).</ResponseField>
<ResponseField name="poll_named" type="boolean">Show who voted for what (only with `poll_require_email`).</ResponseField>
<ResponseField name="poll_closes_at" type="string | null">If set, the poll stops accepting votes after this ISO 8601 datetime.</ResponseField>
<ResponseField name="require_signin" type="boolean">For `people` access, whether invitees must sign in (vs the account-free email-confirm path).</ResponseField>
<ResponseField name="allow_access_requests" type="boolean">For `people` access, whether blocked viewers can request access.</ResponseField>
<ResponseField name="business_only" type="boolean">For `email_gate` access, whether personal email domains are blocked.</ResponseField>
<ResponseField name="allowed_domains" type="string[]">For `email_gate` access, the email domains allowed in (empty = any email).</ResponseField>
<ResponseField name="og_title" type="string | null">Owner-set link-preview (unfurl) title, or `null` to fall back to the page's own OG tags.</ResponseField>
<ResponseField name="og_description" type="string | null">Owner-set link-preview (unfurl) description, or `null` to fall back to the page's own OG tags.</ResponseField>

***

## Create a page

<code>POST /api/v1/pages</code>

Pass **`html`** (or `html_base64` / `html_url`) for a single page, **`files`** for a multi-page
deck, or **`file_base64`** for a PDF or image — one of these per request.

<ParamField body="html" type="string">
  Complete, self-contained HTML document (all assets inlined). For a single page, provide this
  **or** `html_base64` **or** `html_url`. Up to your plan's per-file cap (Free 5 MB, Pro 50 MB,
  Team 100 MB).
</ParamField>

<ParamField body="html_base64" type="string">
  The single page's HTML, base64-encoded. base64 is ASCII, so it can't be mangled in transit —
  use it to send HTML inline without risking emoji/Unicode (ZWJ) corruption. The server decodes it.
  Still inline, so the request body cap applies (\~3–4 MB) — for larger HTML use `html_url`.
  The HTML must be valid UTF-8 and non-empty, or you get a **`400`**.
</ParamField>

<ParamField body="html_url" type="string">
  A public `https` URL the server fetches the single page's HTML from, instead of inlining it —
  so large HTML never passes through the model's token stream. Same SSRF protections as `file_url`
  (private/internal/loopback/metadata hosts blocked, redirects re-checked per hop); up to your
  plan's per-file cap.
</ParamField>

<ParamField body="sha256" type="string">
  Optional. Hex SHA-256 of the HTML **bytes**. The server hashes what it receives (from `html`,
  `html_base64`, or `html_url`) and returns **`422`** on mismatch — a byte-perfect integrity guarantee.
</ParamField>

<ParamField body="files" type="object[]">
  Array of `{ path, html }` for a multi-page deck. Link between pages with relative
  (`team.html`) or root-relative (`/team.html`) paths. Limits: up to 60 files, 2 MB per file,
  12 MB per deck (HTML + assets combined).
</ParamField>

<ParamField body="assets" type="object[]">
  For a deck: non-HTML files its pages reference with relative paths — images
  (PNG/JPG/WebP/GIF/SVG/ICO), CSS, JS, fonts (WOFF/WOFF2/TTF/OTF), JSON. Array of
  `{ path, data_base64 }`, e.g. `{ "path": "images/logo.png", "data_base64": "iVBOR…" }`.
  Up to 60 assets, 5 MB each, within the 12 MB deck total. Assets are served behind the same
  access controls as the deck's pages.
</ParamField>

<ParamField body="file_base64" type="string">
  A **PDF or image** (PNG/JPG/WebP/GIF), shown inline. Base64-encoded; pass `filename` (and optionally
  `mime`). The file must fit your plan's per-file cap (Free 5 MB, Pro 50 MB, Team 100 MB), and inlining
  base64 also adds \~33% to the request body — for larger files prefer `file_url`. SVG, Office docs, and
  spreadsheets aren't supported — export to PDF first.
</ParamField>

<ParamField body="file_url" type="string">
  A public `https` URL to fetch a **PDF or image** from instead of inlining it (up to your plan's
  per-file cap — Free 5 MB, Pro 50 MB, Team 100 MB). Private, internal, loopback, and cloud-metadata
  hosts are blocked, and redirects are re-checked per hop.
</ParamField>

<ParamField body="filename" type="string">
  Original filename for a `file_base64`/`file_url` upload, e.g. `report.pdf` — sets the
  title/download and the file type. Inferred from the URL when omitted with `file_url`.
</ParamField>

<ParamField body="entry" type="string">
  For a deck, which file is the landing page. Defaults to `index.html`, else the shallowest file.
</ParamField>

<ParamField body="title" type="string">Display title.</ParamField>

<ParamField body="access" type="string" default="link">
  `link`, `people`, `private`, `password`, or `email_gate`.
</ParamField>

<ParamField body="allowed_emails" type="string[]">
  Emails allowed to view when `access` is `people`.
</ParamField>

<ParamField body="password" type="string">
  Password to protect the page when `access` is `password`. If omitted, one is generated and returned in the response.
</ParamField>

<ParamField body="publish" type="boolean" default="true">
  When `false`, saves a private draft without going live.
</ParamField>

<ParamField body="indexed" type="boolean" default="false">
  Allow search-engine indexing.
</ParamField>

Every field below is optional — omit them all for a simple share. They can also be set later via [update](#update-a-page).

<ParamField body="expires_at" type="string | null">[Link control](/guides/link-controls) — ISO 8601 datetime after which the link stops working (e.g. `2026-07-01T17:00:00Z`).</ParamField>
<ParamField body="max_views" type="number | null">[Link control](/guides/link-controls) — close the link after this many views.</ParamField>
<ParamField body="download_disabled" type="boolean" default="false">[Link control](/guides/link-controls) — view-only: hide the download button on a PDF/image and force inline viewing.</ParamField>
<ParamField body="notify_on_view" type="boolean" default="false">Email you each time the page is opened.</ParamField>
<ParamField body="og_title" type="string | null">Link-preview (unfurl) title shown when the link is pasted in iMessage/Slack/social. Wins over the page's own OG tags; null/omitted falls back to them.</ParamField>
<ParamField body="og_description" type="string | null">Link-preview (unfurl) description. Wins over the page's own OG tags.</ParamField>
<ParamField body="allow_comments" type="boolean" default="false">Show a comments widget; everyone who can see the page sees the thread.</ParamField>

<ParamField body="comments_require_email" type="boolean" default="false">Comments identity. `false` (default) = anonymous: anyone can comment with no account (a display name is optional). `true` = each commenter confirms a one-time email link first; their email stays private.</ParamField>
<ParamField body="require_signin" type="boolean" default="false">For `people` access, require invitees to sign in instead of the email-confirm path.</ParamField>
<ParamField body="allow_access_requests" type="boolean" default="false">For `people` access, let a viewer who isn't invited request access.</ParamField>
<ParamField body="business_only" type="boolean" default="false">For `email_gate` access, block personal email domains (gmail, etc.).</ParamField>
<ParamField body="allowed_domains" type="string[]">For `email_gate` access, only allow these email domains, e.g. `["acme.com"]`.</ParamField>

<ParamField body="collect_approvals" type="boolean" default="false">**Pro.** Collect email-verified client sign-off — designated approvers Approve / Request changes on the page. Set `approver_emails` too.</ParamField>
<ParamField body="approver_emails" type="string[]">The designated approver emails (used with `collect_approvals`), e.g. `["client@acme.com"]`.</ParamField>
<ParamField body="approvals_visibility" type="string" default="owner">`owner` (decisions private to you) or `shared` (approvers see each other's decisions).</ParamField>
<ParamField body="collect_votes" type="boolean" default="false">Add a one-question poll to the page. Set `poll_question` + `poll_options` too.</ParamField>
<ParamField body="poll_question" type="string | null">The poll question.</ParamField>
<ParamField body="poll_options" type="string[]">The poll options (2–12), e.g. `["Look A","Look B"]`.</ParamField>
<ParamField body="poll_require_email" type="boolean" default="false">Poll identity: `true` = verified email (one per person); `false` = anonymous (one per device).</ParamField>
<ParamField body="poll_results" type="string" default="public">Who sees results: `public`, `vote` (after voting), or `owner` (only you).</ParamField>
<ParamField body="poll_named" type="boolean" default="false">Show who voted for what (only with `poll_require_email`).</ParamField>
<ParamField body="poll_closes_at" type="string | null">ISO 8601 datetime after which the poll stops accepting votes.</ParamField>

A free account that enables a Pro setting (e.g. `collect_approvals`) gets a **`402`** with an explanatory message; the page isn't created. Titles are unique per account — a duplicate returns **`409`**.

```bash theme={null}
curl -X POST https://useshareable.com/api/v1/pages \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "html": "<!doctype html><h1>Hi</h1>", "title": "Demo", "access": "link" }'
```

Returns the [page object](#the-page-object) with status `201`.

```bash Expiring, view-limited link theme={null}
curl -X POST https://useshareable.com/api/v1/pages \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "html": "<!doctype html><h1>Q3 board update</h1>", "access": "password",
        "expires_at": "2026-07-01T17:00:00Z", "max_views": 50 }'
```

```bash Multi-page deck theme={null}
curl -X POST https://useshareable.com/api/v1/pages \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Acme deck",
    "entry": "index.html",
    "files": [
      { "path": "index.html",   "html": "<!doctype html><img src=\"logo.png\"><a href=\"team.html\">Team</a>" },
      { "path": "team.html",    "html": "<!doctype html><h1>Team</h1>" }
    ],
    "assets": [
      { "path": "logo.png", "data_base64": "iVBORw0KGgo…" }
    ]
  }'
```

The deck is served at `/p/{slug}` (the entry file) and each file — assets included — at
`/p/{slug}/{path}`.

***

## List pages

<code>GET /api/v1/pages</code>

Returns an array of the authenticated account's pages, most recently edited first (same order as the dashboard).

<ParamField query="limit" type="number" default="100">
  How many pages to return (1–200). Defaults to 100.
</ParamField>

<ParamField query="offset" type="number" default="0">
  Skip this many results for paging through a large account.
</ParamField>

The response includes a `next_offset` field — an integer to pass as the next `offset` when more
pages remain, or `null` when you've reached the end.

```bash theme={null}
curl "https://useshareable.com/api/v1/pages?limit=50&offset=0" \
  -H "Authorization: Bearer sm_your_key"
```

***

## Retrieve a page

<code>GET /api/v1/pages/{id}</code>

```bash theme={null}
curl https://useshareable.com/api/v1/pages/PAGE_ID \
  -H "Authorization: Bearer sm_your_key"
```

***

## Retrieve a page's HTML

<code>GET /api/v1/pages/{id}/html</code>

Returns the page's **current HTML** — the editable working copy, identical to what's live
unless there are unpublished changes. Read this to patch a page surgically instead of
rebuilding it from scratch.

For a single page the response is `{ kind: "single", html, has_unpublished_changes }`; for a
deck it's `{ kind: "bundle", entry, files: [{ path, html }], has_unpublished_changes }`. A
PDF/image artifact has no HTML and returns **`400`**.

```bash theme={null}
curl https://useshareable.com/api/v1/pages/PAGE_ID/html \
  -H "Authorization: Bearer sm_your_key"
```

***

## Update a page

<code>PATCH /api/v1/pages/{id}</code>

Updating the HTML or `files` changes the **draft** only — call [publish](/api-reference/publishing) to push it live.

<ParamField body="html" type="string">New HTML (saved as draft). Single pages only — not decks. Or send it byte-faithfully with `html_base64` / `html_url`. Up to your plan's per-file cap (Free 5 MB, Pro 50 MB, Team 100 MB). Read the current HTML first with [`GET /pages/{id}/html`](#retrieve-a-pages-html) to patch it in place.</ParamField>
<ParamField body="html_base64" type="string">New HTML as base64 (ASCII-safe — avoids emoji/Unicode corruption). Decoded server-side and saved as draft.</ParamField>
<ParamField body="html_url" type="string">A public `https` URL the server fetches the new HTML from (SSRF-guarded), so large HTML skips the token stream. Saved as draft.</ParamField>
<ParamField body="sha256" type="string">Optional hex SHA-256 of the HTML bytes; **`422`** on mismatch.</ParamField>

<ParamField body="files" type="object[]">
  For a deck, the **full** new set of `{ path, html }` files (saved as draft). Files not included are
  removed; same limits as create (60 files, 2 MB each, 12 MB total). Optionally pass `entry` to set the landing page.
</ParamField>

<ParamField body="entry" type="string">For a deck, re-point the landing page to an existing file path.</ParamField>

<ParamField body="title" type="string" />

<ParamField body="access" type="string">`link`, `people`, `private`, `password`, or `email_gate`.</ParamField>

<ParamField body="allowed_emails" type="string[]" />

<ParamField body="password" type="string">Set/replace the password (when `access` is `password`).</ParamField>

<ParamField body="indexed" type="boolean" />

<ParamField body="expires_at" type="string | null">[Link control](/guides/link-controls) — expiry datetime, or `null` to remove it.</ParamField>
<ParamField body="max_views" type="number | null">[Link control](/guides/link-controls) — view cap, or `null` to remove it.</ParamField>
<ParamField body="download_disabled" type="boolean">[Link control](/guides/link-controls) — view-only for a PDF/image.</ParamField>
<ParamField body="notify_on_view" type="boolean">Email you each time the page is opened.</ParamField>
<ParamField body="allow_access_requests" type="boolean">When `access` is `people`, let a viewer who isn't invited request access — you approve or deny by email or in the app. See [access levels](/publishing-and-access).</ParamField>
<ParamField body="allow_comments" type="boolean">Show a comments widget on the page; everyone sees the thread. Anonymous by default — pair with `comments_require_email: true` to require a verified email. You moderate (hide/delete) from the Share dialog.</ParamField>

<ParamField body="comments_require_email" type="boolean">Comments identity. `false` (default) = anonymous (no account needed; display name optional). `true` = commenters confirm a one-time email link first; their email stays private.</ParamField>
<ParamField body="require_signin" type="boolean">For `people` access, require invitees to sign in.</ParamField>
<ParamField body="business_only" type="boolean">For `email_gate` access, block personal email domains.</ParamField>
<ParamField body="allowed_domains" type="string[]">For `email_gate` access, the allowed email domains, e.g. `["acme.com"]`.</ParamField>
<ParamField body="og_title" type="string | null">Link-preview (unfurl) title. Wins over the page's own OG tags; `null` clears it.</ParamField>
<ParamField body="og_description" type="string | null">Link-preview (unfurl) description. Wins over the page's own OG tags; `null` clears it.</ParamField>
<ParamField body="notify_on_view" type="boolean">Email you each time the page is opened.</ParamField>
<ParamField body="collect_approvals" type="boolean">**Pro.** Collect email-verified client sign-off. Set `approver_emails` too.</ParamField>
<ParamField body="approver_emails" type="string[]">The designated approver emails (used with `collect_approvals`).</ParamField>
<ParamField body="approvals_visibility" type="string">`owner` or `shared`.</ParamField>
<ParamField body="collect_votes" type="boolean">Add a one-question poll. Set `poll_question` + `poll_options` too.</ParamField>
<ParamField body="poll_question" type="string | null">The poll question.</ParamField>
<ParamField body="poll_options" type="string[]">The poll options (2–12).</ParamField>
<ParamField body="poll_require_email" type="boolean">Poll identity: verified email vs anonymous.</ParamField>
<ParamField body="poll_results" type="string">Who sees results: `public`, `vote`, or `owner`.</ParamField>
<ParamField body="poll_named" type="boolean">Show who voted for what (only with `poll_require_email`).</ParamField>
<ParamField body="poll_closes_at" type="string | null">ISO 8601 datetime after which the poll closes, or `null` to remove.</ParamField>

Same error responses as create: enabling a Pro setting on a free account returns **`402`**, and a
duplicate title returns **`409`**.

```bash theme={null}
curl -X PATCH https://useshareable.com/api/v1/pages/PAGE_ID \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "access": "private" }'
```

```bash Add an expiration + view-only theme={null}
curl -X PATCH https://useshareable.com/api/v1/pages/PAGE_ID \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "expires_at": "2026-07-01T17:00:00Z", "download_disabled": true }'
```

***

## Delete a page

<code>DELETE /api/v1/pages/{id}</code>

Permanently deletes the page. Returns status `204`.

```bash theme={null}
curl -X DELETE https://useshareable.com/api/v1/pages/PAGE_ID \
  -H "Authorization: Bearer sm_your_key"
```

***

## Versions

A version is saved automatically on each **publish** and each **edit** (deduplicated by content).
Named versions are kept forever; older unnamed ones are pruned.

### List versions

<code>GET /api/v1/pages/{id}/versions</code>

Returns `{ version_no, source, label, size_bytes, created_at }[]`, newest first. `source` is
`publish`, `edit`, or `restore`.

### Get a version

<code>GET /api/v1/pages/{id}/versions/{version_no}</code>

Returns the version's metadata plus its **full** content — `html` for a single page, or `files` + `entry` for a deck. (The MCP `get_version` tool previews long content by default; pass `full: true` there for the whole thing. To read the *current* page's HTML rather than a past version, use [`GET /pages/{id}/html`](#retrieve-a-pages-html).)

### Restore a version

<code>POST /api/v1/pages/{id}/versions/{version_no}/restore</code>

<ParamField body="publish" type="boolean" default="false">
  When `true`, also publish the restored content live. Otherwise it's restored to the draft only.
</ParamField>

Restoring appends a new `restore` version. Returns the updated [page object](#the-page-object).

```bash theme={null}
curl -X POST https://useshareable.com/api/v1/pages/PAGE_ID/versions/3/restore \
  -H "Authorization: Bearer sm_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "publish": true }'
```

### Name a version

<code>PATCH /api/v1/pages/{id}/versions/{version_no}</code>

<ParamField body="label" type="string | null">A name for the version (named versions are never pruned). `null` clears it.</ParamField>
