---
title: "Patch a page's arrangement and metadata (RFC 6902, optimistic concurrency)."
method: PATCH
path: "/v1/dashboards/{id}/pages/{pageId}"
tags: ["dashboards"]
---

# Patch a page's arrangement and metadata (RFC 6902, optimistic concurrency).

`PATCH /v1/dashboards/{id}/pages/{pageId}`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/dashboards/{id}/pages/{pageId}`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Applies an RFC 6902 JSON Patch to a page's fields document — `/layout` (including the
htmlSlots `layout.html` frame string and per-breakpoint `layout.items.{lg,md,sm,xs}`
grid geometry), `/title`, `/background`, `/theme`, `/doc` (page-level documentation —
`description`/`intent`/`assumptions`/`notes`, ADR-0045 §4, surfaced by
`dvt_dashboard_get(view="docs")` and the docs drawer) — without touching any element row
or its revision history (DVT-2753). This is the non-destructive, surgical alternative
to a full-spec resend (PATCH /v1/dashboards/{id} or dvt_dashboard_apply_spec), which
re-keys every page and element to fresh UUIDs and resets their revision history.

Forbidden paths (400 invalid-patch), because they are owned elsewhere or out of scope
for a geometry/metadata edit: `/position` (use the pages reorder endpoint), `/id` and
`/slug` (immutable — slug uniqueness, comment-anchor remapping, and `<dvt-slot ref>`
targets all key off them), `/kind` (not a geometry edit), `/panels` (separate element
rows, not part of a page's fields document — use the element API), `/hidden` (page
visibility is not patchable on this route, see DVT-2442), `/layout/mode`
(grid/canvas/htmlSlots transitions are out of scope), and a whole-document replace
(`""` or `/`).

Single-page dashboards (409 single-page-field-not-patchable): a dashboard authored in
the single-page shorthand (top-level `layout`/`panels`, no `pages` array) has a
synthesized page whose fields document carries ONLY `layout` — its bundle has no
title/background/theme/doc to receive a write, and the assembled bundle re-emits only
`layout` + `panels` for that shape. Patching anything outside `/layout` on such a page
would silently persist into storage (and version/audit it) without ever reaching the
assembled spec, the renderer, or `dvt_dashboard_get(format="full")` — so it is refused.
Only `/layout` (and its descendants) is patchable on a single-page dashboard's page;
convert to the multi-page form first (a full-spec update with a `pages` array) to patch
title/background/theme/doc.

The body is a pure patch document, so the current page version is supplied
out-of-band: pass it via the `version` query parameter or an `If-Match` header. A
stale version returns 409.

Provenance (DVT-1658): pass an optional `reason` query parameter (or the
`X-Dvt-Change-Reason` header) explaining why the change was made — it is recorded as
the new page revision's change summary. Unlike PatchElement (DVT-1792), a reason is
NOT required here; an unsupplied reason falls back to a generated summary.

## Query parameters

- `version` integer
- `reason` string

## Headers

- `If-Match` string

## Response `200`

Page updated; returns the new page header.

- PageEnvelope
  - `data` Page, required: A single dashboard page header — identity and metadata plus the optimistic-concurrency version. Arrangement (layout) and membership live in the page's stored fields; this view is what page revision/restore operations return.
    - `pageId` string, uuid, required: Durable UUID primary key.
    - `slug` string, required: Readable, dashboard-scoped reference (the authored page id), e.g. "overview".
    - `dashboardId` string, uuid, required
    - `title` string
    - `kind` 'page' | 'modal' | 'popup' | 'drilldown', required
    - `isHidden` boolean, required
    - `position` integer, required: Ordering among sibling pages.
    - `version` integer, required
  - `meta` Meta, required: Response metadata.
    - `requestId` string, required: Correlates with the X-Request-Id header.
    - `total` integer: Item count, present on collection responses.
    - `nextCursor` string, nullable: Opaque keyset-pagination cursor, present on paginated responses (e.g. GET /v1/dashboards) when more rows exist beyond this page. Pass it back as the `cursor` query param to fetch the next page; absent or null on the last page.
    - `roleResolveDegraded` boolean: Snowflake mode only (DVT-2260), on GET /v1/dashboards: present and true when the caller's live Snowflake role resolution failed or was unavailable — role-shared dashboards may be missing from this page. Omitted (not merely false) whenever resolution succeeded or the role-share arm does not apply (off snowflake mode, an org admin/owner, or off the ingress listener).

## Other responses

- `400`: Malformed request.
- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Resource does not exist.
- `409`: Stale page version, or a single-page dashboard (single-page-field-not-patchable — the patch targets a field other than /layout).
- `422`: The spec failed JSON Schema validation. `fields` lists each violation.

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
