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.
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.
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.
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.createand then shared into the channel viacanvases.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_contentmarkdown 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 usecanvases.editwithinsert_at_endoperations after creation. - Standalone canvases require a paid Slack plan (Pro, Business+, or Enterprise Grid). On free workspaces,
canvases.createrequires thechannel_idparameter, forcing every canvas to be tabbed to a channel. The API returnsfree_teams_cannot_create_standalone_canvaseson 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_idsarray 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_idsanduser_idsin the same request — Slack returnsinvalid_parameters. - DM restriction:
canvases.access.setdoes not accept DM or MPDM channel IDs. To share a canvas via DM, useuser_idsinstead. 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,
readaccess is often sufficient. For active living documents,writeis 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.
Step 5: Update the index with document links
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.
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.