Launched:self-serve migrations intoSuperhuman Docs (Coda)
Try it now
01Agent-first
Runs where you already work
Plug it into Claude, ChatGPT or Cursor. Describe the move in plain English; the agent runs it.
02Engineer-led
Our production engine, unlocked
The pipeline our engineers use on managed enterprise migrations — the same code, now something you can drive yourself.
03Pricing
Try 10 pages free, then $1 a page
Credit-based, pay-as-you-go. No scoping call, no quote — sample it on your own docs before you spend anything.
04Sources
NotionSlabConfluenceSoonGoogle DocsSoon
Skip to content

Slack Canvas API: Channel vs Standalone for Document Migrations

Each Slack channel holds one channel canvas. Learn the API pattern for migrating documents at scale using standalone canvases and a channel index.

Nachi Raman Nachi Raman · · 14 min read
Slack Canvas API: Channel vs Standalone for Document Migrations
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,500+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Slack Canvas API: Channel vs Standalone for Document Migrations

A channel canvas is the single canvas pinned to a Slack channel's Canvas tab. A standalone canvas is an independent document that exists outside any channel until you explicitly share it. A tabbed canvas is a standalone canvas that has been pinned to a channel header tab (introduced in the 2025 UI change). When you migrate documents into Slack at scale — from Quip, Confluence, Notion, or any other source — the choice between these types is not a preference. It is an architectural constraint baked into the Slack API: each channel holds exactly one channel canvas, and that limit cannot be increased.

This guide covers the constraint, the decision framework for choosing canvas type, and the exact API-call sequence to land thousands of migrated documents in Slack channels where users can actually find them.

Warning

This is an architectural constraint, not a feature gap. Slack's one-canvas-per-channel limit is fundamental to how channel canvases work — the Canvas tab is the channel canvas. Do not plan a migration that assumes this will change.

Canvas type reference

Property Channel Canvas Standalone Canvas Tabbed Canvas
Scope Bound to one channel Independent document Independent, pinned to channel header
Creation method conversations.canvases.create with channel_id canvases.create (no channel_id) canvases.create with channel_id
Access model Inherits channel membership Invisible until canvases.access.set Inherits channel membership
Plan requirement All plans including Free Paid plans only (Pro, Business+, Enterprise Grid) All plans including Free
Per-channel limit 1 (ever) Unlimited (subject to sharing limits) 15 tabs per channel header
User visibility default Visible to all channel members Invisible to all users Visible to all channel members
Error if limit exceeded channel_canvas_already_exists N/A free_team_canvas_tab_already_exists (Free plans)

Decision tree: which canvas type for your migration

Use this as the entry point before writing any migration code.

Is the target workspace on a paid Slack plan (Pro, Business+, or Enterprise Grid)?
├── NO → Only channel canvases or tabbed canvases are available.
│         One document per channel maximum. If document count >> channel count,
│         this migration cannot be done in Slack canvases at scale.
│         Stop and reconsider destination.
└── YES → How many documents are you migrating per destination channel?
          ├── 1 document per channel → Use channel canvas directly.
          │   Call conversations.canvases.create. Done.
          └── Multiple documents per channel → Use index + standalone pattern.
              One channel canvas as index, one standalone canvas per document,
              canvases.access.set to share each standalone into the channel.
Warning

This is an architectural constraint, not a feature gap. Slack's one-canvas-per-channel limit is fundamental to how channel canvases work — the Canvas tab is the channel canvas. Do not plan a migration that assumes this will change.

Why you cannot use one channel canvas per migrated document

A Slack channel supports exactly one channel canvas. Calling conversations.canvases.create with a channel_id succeeds the first time and returns the new canvas ID. Calling it a second time on the same channel returns channel_canvas_already_exists. This is explicitly documented in Slack's API reference. The existing canvas ID can be found via conversations.info in the channel.properties.canvas field. (docs.slack.dev)

The math is immediate. Migrating 500 Quip documents as channel canvases requires 500 channels. At 10,000 documents, you need 10,000 channels. That is not a migration — it is workspace pollution that cripples discoverability, blows through channel limits, and confuses every user who joins.

On certain plan tiers, the API returns team_tier_cannot_create_channel_canvases instead, blocking channel canvas creation entirely.

Slack's end-user UI also shifted toward canvases in tabs in 2025, with existing channel and DM canvases converting starting April 9, 2025. A channel or DM can have only 15 tabs in its header. Even if you think in tabs rather than legacy channel canvases, you still cannot pin every migrated document into a conversation header. (slack.com, slack.com)

The pattern that works: index canvas + standalone documents

The correct architecture for migrating documents into Slack at volume uses two canvas types together:

  • One channel canvas per channel acting as an index or table of contents for that channel's migrated content.
  • One standalone canvas per migrated document created with canvases.create and then shared into the channel via canvases.access.set.

This keeps each channel's Canvas tab useful — it becomes the navigation layer — while storing the actual document content in standalone canvases that are linked from the index and accessible to channel members.

The channel index holds grouped links, section headings, owners, tags, and dates so users can browse quickly. The standalone canvases hold the actual document content. Channels point to migrated documents; they do not contain them one-by-one as channel-scoped canvases.

If you are evaluating the broader Quip migration path, start with our Quip to Slack Canvases Migration guide.

Required OAuth scopes

Each API method in this pattern requires specific scopes. Missing any of these produces missing_scope errors that are not always descriptive about which scope is absent.

API Method Required Scope(s) Notes
canvases.create canvases:write Bot token sufficient
canvases.edit canvases:write Must be canvas owner or have write access
canvases.access.set canvases:write Bot must own the canvas to set access
canvases.sections.lookup canvases:read Used to check existing content before insert
conversations.canvases.create canvases:write Bot must be channel member for private channels
conversations.info channels:read (public), groups:read (private) Required to retrieve existing canvas ID
Joining private channels groups:write or chat:write Bot must be explicitly invited; groups:write does not auto-join

For private channel operations, the bot must be invited to the channel before any canvas API call. The error channel_not_found appears — not a permissions-specific message — when the bot lacks membership in a private channel.

API-call sequence for the index + standalone pattern

The following sequence assumes you have a bot token with the canvases:write scope installed in the target workspace.

Step 1: Ensure the destination channel is usable

For channel-scoped canvas creation, Slack requires the channel to be public, or the app/user must already be invited to a private channel. If you skip this check, your migration will fail late. The error is not always descriptive — you may get channel_not_found rather than a permissions-specific message on private channels where the app is not a member. (docs.slack.dev)

Step 2: Create the standalone canvas

Call canvases.create for each migrated document. This method creates a standalone canvas owned by the calling app or user. Do not pass channel_id — the canvas should start as a free-floating document.

POST https://slack.com/api/canvases.create
{
  "title": "Q3 Account Plan — Acme Corp",
  "document_content": {
    "type": "markdown",
    "markdown": "# Q3 Account Plan\n## Objectives\n- Expand seat count...\n"
  }
}

The response returns canvas_id (e.g., F1234ABCD). Store this — you need it for access grants and the index.

Key constraints:

  • The document_content markdown field is limited to 1 MiB per call. Canvas tables are capped at 300 cells. If a migrated document exceeds either limit, split it or use canvases.edit with insert_at_end operations after creation.
  • Standalone canvases require a paid Slack plan (Pro, Business+, or Enterprise Grid). On free workspaces, canvases.create requires the channel_id parameter, forcing every canvas to be tabbed to a channel. The API returns free_teams_cannot_create_standalone_canvases on free plans. (slack.com, docs.slack.dev)
  • App ownership and invisibility: a canvas created with a bot token is owned by that bot app. It is invisible to every user in the workspace until you grant access. It will not appear in search results or the Canvases browser. This is the default behavior for app-created standalone canvases, not a bug. (docs.slack.dev)

Step 3: Grant channel access with canvases.access.set

Call canvases.access.set to share the standalone canvas with the target channel. The access_level parameter accepts read, write, or owner — though owner only works with user_ids, not channel_ids. Passing channel_ids with owner returns invalid_arguments.

POST https://slack.com/api/canvases.access.set
{
  "canvas_id": "F1234ABCD",
  "access_level": "write",
  "channel_ids": ["C0567WXYZ"]
}

Constraints and gotchas:

  • The channel_ids array accepts a maximum of 20 channel IDs per call. If you need to share a single canvas with more than 20 channels, batch the calls.
  • You cannot pass both channel_ids and user_ids in the same request — Slack returns invalid_parameters.
  • DM restriction: canvases.access.set does not accept DM or MPDM channel IDs. To share a canvas via DM, use user_ids instead. This is documented but easy to miss when building batch scripts that treat all channel IDs uniformly. (docs.slack.dev)
  • Enterprise Grid limit: a single canvas can be shared with up to 1,000 channels. A documented Enterprise Grid deployment at the University of Michigan confirms this cap. Slack's own public help center does not currently surface the same number, so treat 1,000 as a practical ceiling to validate in your tenant rather than an assumed constant. (teamdynamix.umich.edu)
  • For historical archives, read access is often sufficient. For active living documents, write is required.

This method is rated Tier 3 (50+ requests per minute), more generous than the canvas creation endpoints.

Step 4: Create the channel index canvas

Call conversations.canvases.create once per channel to create the index canvas. This is the canvas that appears in the channel's Canvas tab.

POST https://slack.com/api/conversations.canvases.create
{
  "channel_id": "C0567WXYZ",
  "title": "Migrated Documents Index",
  "document_content": {
    "type": "markdown",
    "markdown": "# Migrated Documents\nThis canvas links to all documents migrated from Quip.\n"
  }
}

Unlike standalone canvases, a channel canvas inherits access from the channel itself — no separate canvases.access.set call is needed. Because this call can only succeed once per channel, handle the channel_canvas_already_exists error gracefully: use conversations.info to get the existing canvas ID, then edit it with canvases.edit.

If the workspace returns team_tier_cannot_create_channel_canvases, create the visible index as a tabbed standalone canvas instead by passing channel_id to canvases.create.

Call canvases.edit on the channel canvas to insert links to each standalone canvas. Before inserting, call canvases.sections.lookup to check whether the link already exists — this prevents duplicate entries on migration retries.

# First: check for existing section to prevent duplicate entries on retry
POST https://slack.com/api/canvases.sections.lookup
{
  "canvas_id": "F9999INDEX",
  "criteria": {
    "contains_text": "Q3 Account Plan — Acme Corp"
  }
}
# If sections array is empty, proceed with insert. If non-empty, skip.
 
# Then: insert if not already present
POST https://slack.com/api/canvases.edit
{
  "canvas_id": "F9999INDEX",
  "changes": [
    {
      "operation": "insert_at_end",
      "document_content": {
        "type": "markdown",
        "markdown": "- [Q3 Account Plan — Acme Corp](https://your-workspace.slack.com/docs/F1234ABCD)\n"
      }
    }
  ]
}

Each change in the changes array is also limited to 1 MiB of markdown. For large batches, accumulate links and write them in a single insert_at_end operation rather than calling canvases.edit once per document.

Group the links by folder, team, process, or lifecycle state — whatever mapping makes the migrated corpus navigable.

Optional: use chat.postMessage to drop the standalone canvas URL into the channel as a message. Slack unfurls canvas links natively, giving users a preview card. This helps with discoverability in active channels where users may not check the Canvas tab.

Plan and tier requirements

Channel and DM canvases are available on all Slack plans, including Free.

Standalone canvases require a paid Slack plan (Pro, Business+, or Enterprise Grid). On free workspaces, canvases.create requires the channel_id parameter, forcing every canvas to be tabbed to a channel. Free teams are also constrained by free_team_canvas_tab_already_exists — limited to one canvas tab per channel. (docs.slack.dev)

The index + standalone pattern requires a paid plan. If you are migrating documents into a free Slack workspace, your only option is one channel canvas per channel — which works only if the number of documents roughly matches your channel structure.

Rate limits, batching, and migration time estimates

Both canvases.create and conversations.canvases.create are Tier 2 methods: 20+ requests per minute. canvases.access.set and canvases.edit are Tier 3: 50+ per minute. Note that Slack's actual enforcement is burst-based and workspace-dependent — these tiers are minimums, not guaranteed ceilings. Plan for the Retry-After header on 429 responses regardless of tier.

Operation API Method Rate Tier Per-Call Limits
Create standalone canvas canvases.create Tier 2 (20+/min) 1 MiB markdown, 300-cell tables
Grant channel access canvases.access.set Tier 3 (50+/min) 20 channel_ids per call
Create channel index conversations.canvases.create Tier 2 (20+/min) 1 per channel (ever)
Update index content canvases.edit Tier 3 (50+/min) 1 MiB per change
Check for duplicate sections canvases.sections.lookup Tier 3 (50+/min) Returns matching sections array

Migration time estimates by document volume

The bottleneck for all migrations is canvas creation at Tier 2 (20+ per minute). These estimates cover API time only, before content preparation, access grants, or index updates. Actual wall-clock time will be longer.

Document Count Canvas Creates (Tier 2, 20/min) Access Grants (batched 20 channels/call, Tier 3) Index Updates Estimated Minimum API Time
100 docs, 1 channel 5 min 1 call (~2 min) 1 batch edit ~8 min
1,000 docs, 1 channel 50 min 50 calls (1 canvas × 1 channel, batched) 1–5 batch edits ~55 min
1,000 docs, 50 channels 50 min 2,500 calls (1 canvas × 50 channels each) 50 edits ~100 min
10,000 docs, 100 channels ~500 min 5,000+ calls 100 edits ~600 min+

The 20-channel-ID cap on canvases.access.set means sharing a single canvas across 100 channels requires 5 API calls. At Enterprise Grid scale with the 1,000-channel maximum, that is 50 calls per canvas just for access grants — plus the Tier 2 constraint on creation. Multi-channel sharing at volume is the dominant bottleneck at Enterprise scale, not document creation.

Warning

State management is mandatory. Do not run a canvas migration as a fire-and-forget script. If canvases.access.set fails due to a rate limit, the standalone canvas remains permanently invisible to users. Your tooling must verify the access grant before marking a document as migrated. A failure at the access step produces an invisible document; a failure at the index step produces an unfindable one.

Error code registry

All error strings referenced in this guide, with their cause and recovery path:

Error Code Method Cause Recovery
channel_canvas_already_exists conversations.canvases.create Channel already has a canvas Retrieve existing ID via conversations.info → channel.properties.canvas; use canvases.edit instead
team_tier_cannot_create_channel_canvases conversations.canvases.create Plan tier blocks channel canvas creation Fall back to tabbed standalone canvas via canvases.create with channel_id
free_teams_cannot_create_standalone_canvases canvases.create Free workspace, no channel_id provided Upgrade plan or use channel/tabbed canvas only
free_team_canvas_tab_already_exists canvases.create Free workspace already has one canvas tab One tab per channel on free plans; cannot exceed
invalid_arguments canvases.access.set owner access level used with channel_ids Use owner only with user_ids
invalid_parameters canvases.access.set Both channel_ids and user_ids passed Send separate requests for each
channel_not_found Any channel-scoped method Bot not a member of private channel Invite bot to channel before API calls
canvas_creation_failed canvases.create Payload exceeds 1 MiB Split document; extract images as hosted URLs
invalid_arguments / invalid_blocks canvases.create Unsupported markdown or nested blocks Strip unsupported elements before sending
missing_scope Any method Required OAuth scope absent Add scope listed in the scope table above

How the native Quip converter falls short

Salesforce's built-in Quip-to-Slack converter lets individual users convert Quip documents into Slack canvases one at a time. Bulk conversion is not supported. The converted canvases are standalone canvases owned by the user who triggered the conversion.

The gap: converted canvases are not attached to any channel. They land in the user's personal canvas list, visible only to that user until manually shared. Salesforce's public FAQ explains how to open converted canvases from the read-only Quip banner, a Slackbot message with a direct link, or Slack search — but does not document automatic placement into a destination channel or tab. Quip permissions are mapped to Slack users by email, and users do not gain elevated permissions through the conversion. (help.salesforce.com)

For a team with 200 Quip documents spread across 15 project folders, this means 200 floating canvases that nobody else can see or find unless each one is individually shared to the right channel. The index + standalone pattern closes exactly this gap by automating placement and building navigable indexes per channel.

Edge cases and failure modes

App-owned canvases after app uninstall: if your migration bot is uninstalled, canvases it created are still owned by the bot. Users with granted access can still view and edit them, but ownership transfer requires the owner access level via canvases.access.set with a user_ids parameter — and only the current owner (the bot) can initiate that transfer. Perform this transfer before decommissioning the bot:

POST https://slack.com/api/canvases.access.set
{
  "canvas_id": "F1234ABCD",
  "access_level": "owner",
  "user_ids": ["U0TARGET123"]
}

Note: owner access level is not valid with channel_ids. Transfer ownership to a specific user, then use canvases.access.set with channel_ids and write to maintain channel access.

Canvas content exceeding 1 MiB: the API returns canvas_creation_failed. Long Quip documents with embedded images (as base64) or extensive tables hit this. Extract images as hosted URLs and reference them as markdown image links instead of embedding binary data.

Data fidelity from Quip: complex nested tables, embedded live Salesforce records, and multi-column layouts do not translate 1:1 into Slack Canvases. The canvases.create endpoint rejects payloads containing unsupported markdown or excessively nested blocks, returning invalid_arguments or invalid_blocks. Your migration pipeline must parse the source document, strip or flatten unsupported elements, and format the payload for Slack's supported markdown subset before making the API call.

Public channel visibility gotcha: if you share a canvas set to "Only invited people can access" in a public channel, it becomes visible to everyone in the workspace or Enterprise organization. Do not post canvas links into public channels during validation if you need tighter access control. (slack.com)

Duplicate index entries on retry: if your script retries a canvases.edit call after a timeout, you may get duplicate links in the index. Use canvases.sections.lookup to check for existing content before inserting (see Step 5 above), or design your index markdown to be fully replaced on each update rather than appended incrementally.

Documents needing multi-channel placement: a single standalone canvas can be shared into multiple channels via repeated canvases.access.set calls, batching up to 20 channel_ids per call. For a document that belongs in 60 channels, this requires 3 calls. The Enterprise Grid cap of 1,000 channels per canvas applies as the absolute ceiling. This is the correct approach for shared policy documents, templates, or reference materials that span multiple teams — create one canvas, grant access to all relevant channels, and link it from each channel's index.

Design for the long term

The one-canvas-per-channel constraint shapes every document migration into Slack. The Canvas tab is a single document surface, not a folder.

The index + standalone pattern aligns with this design instead of fighting it. The channel canvas becomes navigation; standalone canvases hold content; canvases.access.set makes those documents visible in the right channels; canvases.sections.lookup keeps the index idempotent on retries; and ownership transfer before bot decommissioning ensures documents remain accessible after the migration tooling is retired.

If you are migrating from Quip specifically, the official Salesforce path handles individual document conversion but leaves organization as an exercise for the reader. The Quip End-of-Life Playbook covers whether Slack is even the right destination for your content.

Frequently Asked Questions

Can a Slack channel have more than one channel canvas?
No. Each Slack channel supports exactly one channel canvas. Calling conversations.canvases.create a second time on the same channel returns the error channel_canvas_already_exists. To add more documents, create standalone canvases and share them to the channel via canvases.access.set.
What is the difference between a Slack channel canvas and a standalone canvas?
A channel canvas is the single canvas pinned to a channel's Canvas tab — access is tied to channel membership. A standalone canvas is an independent document created with canvases.create that is invisible until you explicitly grant access via canvases.access.set. Standalone canvases require a paid Slack plan.
Are standalone Slack canvases available on the free plan?
No. Standalone canvases require a paid Slack plan (Pro, Business+, or Enterprise Grid). On free workspaces, canvases.create requires a channel_id parameter, meaning every canvas must be tabbed to a channel. Channel and DM canvases are available on all plans.
Does the native Quip-to-Slack converter attach canvases to channels?
No. Salesforce's native converter produces standalone canvases owned by the user who triggered the conversion. The canvases are not attached to any channel and must be manually shared or programmatically placed to be discoverable by the team.
How many channels can a standalone Slack canvas be shared with?
A documented Enterprise Grid deployment confirms the cap at 1,000 channels. Slack's public help center does not currently surface this number, so validate it in your own tenant. The canvases.access.set method accepts a maximum of 20 channel_ids per API call, so sharing to more channels requires batching.

More from our Blog