---
title: "Slack Canvas to Confluence Migration: APIs, Limits & Mapping"
slug: slack-canvas-to-confluence-migration-apis-limits-mapping
date: 2026-10-01
author: Abdul Aleem
categories: [Slack Canvas, Confluence, Migration Guide]
excerpt: "Slack Canvas stores no hierarchy, folders, or ordering. Migrating to Confluence means manufacturing structure, converting markdown to XHTML, and handling every gap Canvas never filled."
tldr: "Slack Canvas has no page tree, no version import path, and no export wizard. Migration to Confluence requires building structure from channel relationships and converting markdown to storage format XHTML."
canonical: https://clonepartner.com/blog/slack-canvas-to-confluence-migration-apis-limits-mapping
---

# Slack Canvas to Confluence Migration: APIs, Limits & Mapping


# Slack Canvas to Confluence Migration: APIs, Limits & Mapping

Migrating from Slack Canvas to Confluence is not a data migration — it is a data manufacturing job. Canvas stores almost no structural metadata: no parent-child relationships, no folder paths, no sibling ordering. The hierarchy you want in Confluence has to be designed by your team, not extracted from the source.

Just as with [migrating from Confluence to Slack Canvas](https://clonepartner.com/blog/blog/confluence-to-slack-canvas-migration-the-technical-guide), there is no export wizard, no Atlassian importer, and no third-party connector. You read canvases through the Slack API, convert markdown to Confluence's XHTML-based storage format, and push pages through the Confluence REST API — handling every structural and formatting gap along the way.

This guide covers the full technical path: inventory, structure derivation, format conversion, comment handling, version history limits, and attachment re-upload.

## How Do You List All Slack Canvases?

**There is no `canvases.list` endpoint.** Whether you are moving to Confluence or [migrating Slack Canvas to Notion](https://clonepartner.com/blog/blog/slack-canvas-to-notion-migration-api-limits-data-mapping), the only programmatic way to enumerate canvases across a Slack workspace is the [`files.list`](https://docs.slack.dev/reference/methods/files.list/) method filtered to the `canvas` type.

```bash
curl -s "https://slack.com/api/files.list?types=canvas&count=100" \
  -H "Authorization: Bearer $SLACK_TOKEN"
```

`files.list` is a **Tier 3 endpoint**. Slack's Tier 3 rate limit is defined as 1 call per second per app per workspace, with burst tolerance — not a flat 50 requests per minute. In practice, sustained crawls at 50 requests per minute stay within Tier 3 headroom, but the actual throttle is per-app-per-workspace and will vary under concurrent load. Results are paginated at a default of 100 items per page. Each result is a file object with `id`, `title`, `created`, `updated`, and the channels it was shared into — but no parent reference, no folder path, and no sort order.

> [!WARNING]
> **Scope matters.** A bot token with `files:read` only sees canvases in channels the bot has been added to. A user token sees canvases the user can access. For a complete inventory, use an admin-level user token (Enterprise Grid supports Org-Level User Tokens with admin privileges) or programmatically add the migration bot to all channels before running the crawl. Canvases in private channels or DMs the token cannot reach will silently be missing from results.

To read the actual content of a canvas, you need the `canvases:read` scope. Use `files.info` to get the download URL, or `canvases.sections.lookup` to query specific sections. The content comes back as markdown — not HTML, not JSON blocks. Slack caps canvas content at **1 MiB per `document_content` object**. ([docs.slack.dev](https://docs.slack.dev/surfaces/canvases/))

If you are working from **Slack exports** instead of live API calls, the export format gives you a second inventory surface: `canvases.json`. That file contains each canvas's metadata, a `url_private_download` value for retrieving the content, and a `shares` array showing where the canvas was shared. Comments on canvases are exported separately and must be correlated through `file_conversations.json` and the matching `FC:<canvas_id>` folder. ([slack.com](https://slack.com/help/articles/220556107-How-to-read-Slack-data-exports))

### What metadata does a canvas carry?

| Field | What it tells you | What it does not tell you |
|---|---|---|
| `id` | Unique file ID (starts with `F`) | — |
| `title` | Canvas title | No guaranteed uniqueness |
| `channels` | Channel IDs where canvas was shared | No hierarchy or ordering |
| `created` / `updated` | Timestamps | No version count |
| `user` | Creator's Slack user ID | No "owner" vs "editor" distinction |
| `is_channel_space` / `linked_channel_id` | Whether this is a channel-native canvas | — |

There is no `parent_id`, no `folder`, no `position`, and no `space` equivalent. A canvas is a flat object floating in Slack's file system. Every bit of hierarchy you need in Confluence must be derived or decided.

> [!NOTE]
> **Canvas format change:** Slack began converting channel and DM canvases to canvases in tabs on April 9, 2025. Depending on when you migrate, expect a mixed estate: converted channel-linked canvases from the older model plus standalone canvases shared into channel tabs. The share relationship is still the best structural signal you get from the source. ([slack.com](https://slack.com/help/articles/21290478840979-Feature-change-notice--Channel-canvases))

## Channel Canvases vs. Standalone Canvases

**A channel canvas is the single built-in canvas attached to a Slack channel.** Every channel and DM gets exactly one. It is created via `conversations.canvases.create`, and calling that method a second time on the same channel returns a `channel_canvas_already_exists` error. Access is tied to channel membership — no separate sharing needed.

**A standalone canvas is a free-floating document** created via `canvases.create`. It belongs to the user who created it and is invisible to everyone else until explicitly shared to channels or users via `canvases.access.set`. Standalone canvases require a paid Slack plan.

For migration inventory, this distinction matters because:

- Each channel contributes **at most one channel canvas** plus **zero or more standalone canvases** that were shared into it.
- The `channels` array on a file object tells you which channels a standalone canvas was shared into — this share relationship is the closest thing to structure the Slack API gives you.
- A standalone canvas shared into multiple channels appears in `files.list` once but has multiple channel IDs, so you need to decide which Confluence parent page it belongs under.
- The `is_channel_space` and `linked_channel_id` fields let you programmatically separate channel canvases from standalone canvases shared into conversations. ([docs.slack.dev](https://docs.slack.dev/reference/objects/file-object/))

## The Confluence Page Tree Is a Decision, Not Data You Migrate

Slack Canvas has no concept of a page tree. [Confluence is built around a strict space → page → child page hierarchy](https://clonepartner.com/blog/blog/slack-canvas-vs-confluence-architecture-limits-and-migration). Your migration team must **design** the Confluence structure based on signals from the source.

Budget time for information architecture decisions before writing migration code. The page tree will not match any existing structure in Slack because that structure never existed.

### Four hierarchy-derivation strategies

| Strategy | How it works | When to use it | Main risk |
|---|---|---|---|
| **Channel-per-space mapping** | Each Slack channel becomes a Confluence space (or top-level page). Channel canvas → space home page. Standalone canvases shared into that channel → child pages. | Clean channel taxonomy with minimal cross-sharing | Standalone canvases shared to many channels become ambiguous |
| **Index canvas parsing** | Identify a "master" or "index" canvas with links to other canvases. Parse markdown links to derive ordering and grouping. | Workspaces with a known index document | Assumes index exists and is current; requires validation |
| **Naming convention extraction** | Regex-parse titles like `[Project] - Design Spec` or `Q3 OKRs - Engineering` into a two-level hierarchy. | Workspaces with consistent naming discipline | Fragile — audit coverage before automating |
| **Flat import + post-migration restructuring** | Import all canvases as siblings under a single parent. Let content owners organize in Confluence afterward. | Large volumes with unclear ownership | Pushes organizational work to content owners at a busy moment |

If a standalone canvas is shared across multiple channels, you must decide whether to duplicate it in Confluence (creating maintenance overhead) or assign it to a single primary space. Assign based on the channel where it was first shared — use the earliest timestamp in the `shares` object — and generate Confluence cross-links for the other spaces.

Slack sharing rules are not a clean fit for Confluence permissions. A canvas shared in a public channel becomes visible to everyone in the workspace or Enterprise org. Audit sharing intent manually rather than copying share relationships blindly into Confluence restrictions. ([slack.com](https://slack.com/help/articles/15678967614611-Manage-access-permissions-for-canvases-and-lists))

## How to Convert Canvas Markdown to Confluence Storage Format

**Confluence storage format** is Confluence's persisted XHTML-based body representation — XML with custom `ac:` and `ri:` namespace elements for macros, resource identifiers, and layouts. Canvas content is markdown. The conversion is the core technical work of this migration.

The Slack Canvas API represents content as a `document_content` object with `type: "markdown"`. Do not write a markdown parser from scratch. Use an existing library (`markdown-it` for JavaScript, Python's `markdown` module) to produce HTML, then post-process the HTML into valid Confluence storage format. The post-processing layer handles Confluence-specific elements: `ac:structured-macro` for code blocks, `ac:link` for internal links, `ri:attachment` for inline images.

### Canvas element → Confluence storage format mapping

| Canvas element | Confluence storage format | Edge case / constraint |
|---|---|---|
| `# H1` | `<h1>` | Canvas max is H3; Confluence supports H1–H6 |
| `## H2` | `<h2>` | H4–H6 must be authored post-migration |
| `### H3` | `<h3>` | Deepest canvas heading level |
| `**bold**` | `<strong>` | Straightforward |
| `*italic*` | `<em>` | Straightforward |
| `~~strikethrough~~` | `<s>` | Straightforward |
| `- item` / `1. item` | `<ul>` / `<ol>` | Straightforward |
| `- [ ]` / `- [x]` | `<ac:task-list>` / `<ac:task>` | Or plain `<ul>` with status indicator if not using Confluence tasks |
| ` ``` code ``` ` | `<ac:structured-macro ac:name="code">` | Language hint preserved if specified |
| `> blockquote` | `<blockquote>` | Straightforward |
| `---` divider | `<hr/>` | Straightforward |
| `[text](url)` | `<a href="url">text</a>` | External links stay plain; internal canvas links need remap |
| Table (≤300 cells) | `<table><tbody><tr><th/><td/>` | No formulas, no column types |
| `<@U12345678>` mention | `<ac:link><ri:user ri:account-id="..."/></ac:link>` | Requires Slack UID → Atlassian Account ID lookup via `users.info` |
| Slack unfurl / rich preview | Plain `<a href>` or Atlassian Smart Link macro | See Smart Link macro XML below |
| Inline image (`! [](url_private)`) | `<ac:image><ri:attachment ri:filename="..."/></ac:image>` | Must download, re-upload, then rewrite |

### Heading limitations

Canvas supports only **H1, H2, and H3**. Confluence supports H1–H6. If canvases use bold text or bulleted lists to represent deeper document hierarchy — a common authoring workaround — review those sections manually and decide whether to introduce H4–H6 headings or split oversized canvases into separate child pages.

### Table constraints

Canvas tables are strictly visual grids capped at **300 cells** (e.g., 10 columns × 30 rows) with no column types, calculated fields, or conditional formatting. Conversion to Confluence XML is straightforward:

```xml
<table>
  <tbody>
    <tr>
      <th><p>Endpoint</p></th>
      <th><p>Rate Limit</p></th>
    </tr>
    <tr>
      <td><p>files.list</p></td>
      <td><p>Tier 3 — 1 call/sec/app/workspace</p></td>
    </tr>
  </tbody>
</table>
```

Any canvas table approaching the 300-cell limit should be reviewed for whether it belongs in a Confluence table or should become an attached spreadsheet.

### Rewriting mentions

Slack mentions use internal references like `<@U12345678>`. You have two options: strip to plain-text display names (resolving the user ID via `users.info`), or map to Confluence user mentions by matching Slack User IDs to Atlassian Account IDs:

```xml
<ac:link><ri:user ri:account-id="5d41d8cd98f00b204e9800998ecf8427"/></ac:link>
```

### Rewriting link unfurls with Smart Link macro

Slack link unfurls — rich previews of Jira tickets, GitHub PRs, and similar URLs — are proprietary and do not export. In Confluence, wrap these URLs in the Smart Link macro for inline card rendering:

```xml
<ac:structured-macro ac:name="atlassian-smart-link">
  <ac:parameter ac:name="url">https://your-domain.atlassian.net/browse/PROJ-123</ac:parameter>
</ac:structured-macro>
```

Without this, converted unfurls render as plain hyperlinks. Either approach is acceptable; the Smart Link macro is only worth the overhead if the linked resource is in an Atlassian product the Confluence space already integrates with.

### Checklists

Canvas checkboxes (`- [ ]` / `- [x]`) map to Confluence's task list macro:

```xml
<ac:task-list>
  <ac:task>
    <ac:task-status>incomplete</ac:task-status>
    <ac:task-body>Review API rate limits</ac:task-body>
  </ac:task>
  <ac:task>
    <ac:task-status>complete</ac:task-status>
    <ac:task-body>Inventory all canvases</ac:task-body>
  </ac:task>
</ac:task-list>
```

Use this only if your team actively uses Confluence tasks. Otherwise, convert to plain `<ul>` with a ✓ or ☐ prefix — simpler to maintain and less likely to generate spurious task notifications.

## Why Canvas Comments Can Only Become Footer Comments

**Slack Canvas comments are not stored as canvas annotations.** They live as regular channel messages — replies in the thread where the canvas was shared. The Slack API returns them via `conversations.history` and `conversations.replies`, not through any canvas-specific comment endpoint. In Slack exports, these comments appear in separate `FC:<canvas_id>` folders. ([slack.com](https://slack.com/help/articles/203950418-Use-a-canvas-in-Slack))

This has a direct consequence for Confluence. You can create **footer comments** via the API (`POST /wiki/api/v2/pages/{id}/footer-comments`), but you cannot programmatically create **inline comments**. Confluence's inline comment API requires `textSelection`, `textSelectionMatchCount`, and `textSelectionMatchIndex` that reference exact character offsets in the page body. Canvas comments reference channel message timestamps, not character positions in the canvas — there is no mapping between the two models.

The practical approach:

1. Identify the file ID of the canvas.
2. Query `conversations.replies` using the canvas share timestamp to pull the comment thread.
3. Format each message with the original author, Slack user ID, and ISO timestamp for traceability.
4. Push to Confluence: `POST /wiki/api/v2/pages/{page-id}/footer-comments`.
5. Prefix each comment body with metadata: `"Originally posted by @username on YYYY-MM-DD:"` so the historical conversation is legible without Slack access.

## Can You Import Version History into Confluence?

**No.** Confluence's REST API creates every page at version 1. There is no endpoint to insert historical versions, backdate edits, or manufacture a version timeline. The only mechanism that preserves version history in Confluence is the **XML space import** — and that requires the source to be a Confluence space export, which Slack Canvas is not.

Confluence's version-related endpoints (`GET /wiki/api/v2/pages/{id}/versions`) are read-only. There is no `POST` or `PUT` path for versions in either V1 or V2 of the Confluence REST API. This is not a documentation gap — it is a deliberate constraint.

On the Slack side, canvas version history is available through workspace data exports for customers with export rights for all conversations. Enterprise Grid customers can retrieve canvas version history via the Discovery API. But even with every historical version extracted, Confluence provides no API path to import them as page versions.

> [!CAUTION]
> **Do not promise imported native page history in Confluence.** You can preserve Slack version history as external evidence — JSON snapshots, HTML renders, PDFs, or attached audit files — but you cannot backfill Confluence's built-in version timeline through any documented API.

### Realistic version history options

| Option | What it preserves | Effort | Native Confluence history? |
|---|---|---|---|
| Acknowledge the loss | Nothing | None | No |
| Append version log table | Author names + timestamps from Slack | Low–medium | No — human-readable only |
| Archive canvas as PDF attachment | Point-in-time visual snapshot | Low | No |
| Export JSON history from Discovery API | Full diff history as external artifact | High | No |

For high-value canvases, combine the version log table with a PDF attachment. This gives compliance reviewers a human-readable audit trail and a visual reference without requiring Slack access after decommission.

## How to Handle Attachments and Inline Images

Images and files embedded in a Slack Canvas are hosted on Slack's CDN as standard Slack files. In canvas markdown they appear as private URLs (`https://files.slack.com/...`) requiring authentication. These URLs break the moment your Slack workspace is decommissioned — or immediately for anyone without a valid Slack session.

The migration is a **three-step process per image**: download, upload, rewrite reference. It must happen after the page is created because the attachment endpoint requires a page ID.

**Step 1: Download** using the `url_private_download` link from `files.info`, passing your Slack bot token in the `Authorization` header.

**Step 2: Upload to Confluence** using the **V1 attachment endpoint**. The Confluence V2 API is GET-only for attachments — there is no `POST` endpoint to upload files in V2. Use the V1 content-attachments API:

```bash
curl -X POST \
  "https://your-domain.atlassian.net/wiki/rest/api/content/{pageId}/child/attachment" \
  -H "X-Atlassian-Token: nocheck" \
  -H "Authorization: Basic $BASE64_CREDENTIALS" \
  -F "file=@image.png" \
  -F "comment=Migrated from Slack Canvas"
```

**Step 3: Rewrite the page body** to reference the uploaded attachment using Confluence's resource identifier syntax:

```xml
<ac:image>
  <ri:attachment ri:filename="image.png" />
</ac:image>
```

> [!WARNING]
> **Idempotency warning:** Confluence overwrites an attachment if you upload a file with the same name to the same page. This is useful for controlled reruns but dangerous if your migration script is not idempotent by design. Track uploaded file names per page and handle collisions explicitly — either by hashing the filename or by checking existing attachments before uploading.

### Estimating attachment volume

Before writing the download loop, query each canvas's `files.info` response for the `num_files` count and sum across all canvases. A workspace with 200 canvases averaging 5 embedded images each means 1,000 sequential download-upload-rewrite cycles. At 1 second per image (generous for large files), that is roughly 17 minutes of serial I/O. Parallelize with a thread pool capped below Slack's Tier 3 limit, and handle transient 429s with exponential backoff.

## Migration Sequence: Putting It Together

| Step | Action | Key decision / risk |
|---|---|---|
| 1. Inventory | `files.list?types=canvas`, paginate, record all canvas IDs, titles, creators, channels, timestamps | Classify channel vs. standalone using `is_channel_space` and `linked_channel_id` |
| 2. Design page tree | Map channels to spaces or parent pages. Assign multi-channel canvases to single primary space | Get stakeholder sign-off before writing code |
| 3. Extract content | Retrieve markdown via `canvases:read`. Resolve @mentions via `users.info`. Download embedded images via `url_private_download` | Auth scope gaps cause silent content omission |
| 4. Convert | Transform markdown → Confluence XHTML. Apply element mapping table above. Split canvases that exceed logical document size | Heading depth, mention rewriting, checklist format |
| 5. Create pages | `POST /wiki/api/v2/pages` with `parentId` to establish hierarchy | Page order is explicit — Confluence does not infer it |
| 6. Upload attachments | V1 API per page. Update page body to reference `ri:attachment` | V2 has no POST for attachments; handle name collisions |
| 7. Add comments | Extract `conversations.replies` threads. `POST /wiki/api/v2/pages/{id}/footer-comments` with author + timestamp prefix | Inline comments are not mappable from canvas data |
| 8. Stamp traceability | Store Slack canvas ID, channel ID, export timestamp as page properties or labels | Required for reruns, audits, and reconciliation |
| 9. Validate | Test navigation, permissions, broken links, missing images, attachment rendering, page order | A correctly formatted page in the wrong tree is a failed migration |

### Rough effort model

Use this as a baseline for scoping, not a guarantee:

- **< 50 canvases, minimal attachments, single channel:** 2–4 engineer-days for scripting and QA.
- **50–300 canvases, moderate attachments, multi-channel:** 2–4 weeks including information architecture design, script development, QA, and stakeholder review.
- **300+ canvases with complex cross-linking, comment threads, and compliance requirements:** 4–8 weeks. Page tree design and comment correlation typically consume more time than the data transfer itself.

The dominant cost driver is not the volume of canvases — it is the degree of cross-channel sharing and comment thread depth. A workspace with 100 canvases all shared into a single channel is faster to migrate than 50 canvases each shared into 10 channels with active comment threads.

> Migrating Slack Canvases to Confluence and want to get the page tree, formatting, and attachments right the first time? Let's scope it together.
>
> [Talk to us](https://clonepartner.com/talk-to-us?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### Is there a Slack canvases.list API endpoint?

No. There is no dedicated canvases.list method. Slack's developer docs direct you to files.list filtered to the canvas type. This is a Tier 3 endpoint (roughly 50 requests per minute) returning paginated results. A bot token only sees canvases in channels the bot has joined.

### Can you import Slack Canvas version history into Confluence?

No. Confluence's REST API creates every page at version 1 with no way to insert historical versions or backdate edits. The only mechanism that preserves version history is XML space import, which requires a Confluence source. Canvas version history must be archived externally.

### Can Slack Canvas comments become Confluence inline comments?

Not faithfully. Canvas comments are thread-style channel messages, not text-anchored annotations. Confluence inline comments require exact text-selection anchors (textSelection, textSelectionMatchCount) that Slack does not export. Footer comments are the only reliable target.

### What is the difference between a channel canvas and a standalone canvas?

A channel canvas is the single built-in canvas attached to a Slack channel — exactly one per channel, with access tied to channel membership. A standalone canvas is a free-floating document created separately and shared explicitly to channels or users. Only paid Slack plans support standalone canvases.

### Which Confluence API uploads attachments — V1 or V2?

V1. The Confluence V2 API is GET-only for attachments with no POST upload endpoint. Use the V1 content-attachments API: POST /rest/api/content/{pageId}/child/attachment with multipart form data and the X-Atlassian-Token: nocheck header.
