---
title: "Save the canvas (append a draft version)"
method: PUT
path: "/dashboards/{dashboard_id}/canvas"
tags: ["Dashboard v3"]
---

# Save the canvas (append a draft version)

`PUT /dashboards/{dashboard_id}/canvas`

## Request

- Base URL: `https://app.mammoth.io/api/v2`
- URL: `https://app.mammoth.io/api/v2/dashboards/{dashboard_id}/canvas`
- Auth: one of:
  - HTTP bearer
  - API key in header `X-API-KEY` and API key in header `X-API-SECRET` and API key in header `X-WORKSPACE-ID`

Validates the CanvasV3 payload, appends a new draft head (optimistic concurrency via base_sequence → 409 on a moved head), and kicks the async bake. The previous baked bundle keeps serving, flagged stale, until the bake lands.

## Path parameters

- `dashboard_id` integer, required

## Request body

- SaveCanvasSpec: Canvas save parameters
  - `params` SaveCanvasParams, required
    - `canvas` object, required: The CanvasV3 payload
    - `base_sequence` integer, nullable: Optimistic-concurrency token: the draft sequence this edit was based on. 409 when the head has moved past it.
    - `activity` object, nullable: Optional manual-edit activity turn (round-26 §3.3): when present the save appends ONE `changed:true` transcript row for this manual/design edit — `{label, icon, scopes, change, client_activity_id}` the FE already computes — so the ghost becomes a real, seq-ordered, reload-durable timeline turn (with `version_before`/`version_after` from this save). The FE coalesces its own bursts before sending: one persist = one row. Omit for a save that shouldn't add a timeline turn.
    - `prefer` 'mine' | 'theirs' | 'null', nullable: When the head moved past `base_sequence` and both sides changed the same thing: settle it this way (the editor's 'Use mine' / 'Keep theirs') instead of answering 409. Everything else still merges.
    - `base_canvas` object, nullable: The canvas this edit was made on top of, when that is NOT the stored version at `base_sequence`: after a `merged` save the editor still holds what it sent, without the colleague's change. Sent with `base_sequence` = that merged save's sequence; the save is then merged from this canvas, so the editor's own earlier save is never read as a colleague's.
    - `loaded_sequence` integer, nullable: With `base_canvas`: the version the editor's model was loaded at. Only names who changed things since (in a 409 or `merged_moves`); the merge itself runs from `base_canvas`.
    - `prefer_for_head` integer, nullable: With `prefer`: the head sequence of the 409 being answered. The choice applies only while the head is still there — if a colleague saved again since, the person is asked afresh instead of their answer overwriting a change they never saw.
    - `accepts_merge` boolean: The client adopts a `merged` answer (it keeps the merged head before its next full-canvas save). Only such a client's stale save is merged; any other keeps the 409, since writing its un-merged model next would revert the colleague's change without a conflict.

## Response `200`

Request fulfilled, document follows

- SaveCanvasResponse
  - `sequence` integer, required: The new draft head sequence
  - `merged` boolean: True when the head had moved past `base_sequence` and this save was merged onto it (a colleague's changes are in the new head too): the client must adopt the head before sending its next full canvas.
  - `merged_moves` DraftHeadResponse
    - `head_sequence` integer, required: The current draft head sequence
    - `base_sequence` integer, nullable: The `since` the moves are counted from
    - `count` integer, required: Versions written after `since` (uncapped)
    - `moves` DraftMove[]: Newest first, at most `limit`
      - `sequence` integer, required: The version this write produced
      - `user_id` integer, nullable: Who wrote it
      - `user_name` string: Their display name
      - `at` string, nullable: When (ISO 8601, UTC)
      - `kind` 'edit' | 'assistant' | 'sync', required: A manual/server edit, an assistant turn, or a data sync
      - `label` string, nullable: The timeline's own words for the change, if any
  - `bake_job_id` integer, required: Job id of the async bake
  - `recomposed` boolean: True when a content page's structure `type` changed vs the stored head and its architect tiles were re-picked for the new archetype (Design → Structure). False for an ordinary save.
  - `changed_tiles` integer: How many architect tiles entered or left the composition across the recomposed pages. 0 with `recomposed=True` is a legitimate tie (the new archetype re-picks the same charts for this data → 'layout already fits'); >0 drives the 'N charts updated' signal.
  - `seeded_pages` string[]: Ids of brand-new EMPTY authored pages this save auto-seeded with a starter composition (the '+ add page' affordance lands pages via this PUT with no chat turn, so an authored page would otherwise bake to an empty tab). Non-empty means the persisted head now differs from what the client PUT — the client should refetch the draft canvas (GET /{id}/canvas) to pick up the seeded tiles.
  - `format_floor` FormatFloorApplied
    - `filters_added` integer: Auto-derived filter-sidebar entries added (0 when the board already declared filters)
    - `pages_added` string[]: Titles of content pages added to meet the new style's page floor
    - `cover_added` boolean: True when a document cover page was prepended (document only)
    - `charts_composed` integer: Charts composed for a board that had NONE. A Q&A board switched to dashboard has no content page at all, so nothing carries over and no other rule would fill it; the overview is composed to the archetype's target using the same rule the create path uses. 0 whenever the board already had charts — those are never touched.

## Other responses

- `202`: Canvas appended; bake running asynchronously
- `400`: Bad request syntax or unsupported method
- `403`: Request forbidden -- authorization will not help

---

[API](https://skmtc.dev/mammoth/apis/mammoth-analytics.md) · [All operations](https://skmtc.dev/mammoth/apis/mammoth-analytics/llms.txt) · [OpenAPI document](https://skmtc.dev/mammoth/apis/mammoth-analytics/revisions/5dfd1c296ded?raw)
