Slack Canvas API: What Works on a Free Workspace, Tested
We created a channel canvas with the Slack API, wrote markdown into it, found sections and edited them, then read it back. The free-plan errors, the markdown that rendered and the mention syntax that did not.
On this page
The Slack canvas API has two create methods. canvases.create makes a standalone canvas, and conversations.canvases.create makes the canvas tab of a channel. Both take markdown in document_content. You change a canvas with canvases.edit, find a section with canvases.sections.lookup, and read it back as a file. We ran each call on 1 October 2026 in a test workspace on the free plan, with a bot token that had canvases:write, canvases:read and files:read.
Creating a canvas on the free plan
| Call | Token | Response |
|---|---|---|
canvases.create | bot | {"ok": false, "error": "free_teams_cannot_create_standalone_canvases"} |
canvases.create | user | {"ok": false, "error": "free_teams_cannot_create_standalone_canvases"} |
conversations.canvases.create | bot | {"ok": true, "canvas_id": "F0C5FC820DD"} |
conversations.canvases.create, same channel again | bot | {"ok": false, "error": "free_team_canvas_tab_already_exists"} |
On a free workspace, an app can make one canvas per channel and no standalone ones. Our bot had created the channel, so it was a member. The plan limits are in our Slack free plan page.
Who can open each of these canvases, and how canvases.access.set changes that, is on are Slack canvases private.
Which markdown renders
We sent this as {"type": "markdown", "markdown": "..."} to conversations.canvases.create. U0B7L4YK420 is our user and C0C5QG3TQKV the channel:
# Release checklist
Intro paragraph with **bold**, _italic_, ~strike~ and `code`.
## Steps
- First bullet
- Second bullet
- Nested bullet
1. Numbered one
2. Numbered two
- [ ] Open task
- [x] Done task
| Owner | Status |
| --- | --- |
| Ana | Ready |
| Ben | Blocked |
> A quote line
```
code block line
```
Mention:  and channel 
Link: [slack.green](https://slack.green)
---
:rocket: emoji shortcode
Headings, bold, italic, inline code, nested bullets, numbered lists, both checklist states, the table, the quote and the code block all rendered:
The rest also worked: the  mention became a user chip, the same form with # and the channel ID a channel link, :rocket: an emoji, and --- a divider. One thing did not. ~strike~ with single tildes stayed as text. We then added a line with canvases.edit, and ~~strike~~ with double tildes was struck through:
The H1 in the markdown did not become the canvas title. The channel tab said "Untitled" and the canvas showed the grey "Your canvas title" placeholder until we called canvases.edit with {"operation": "rename", "title_content": {"type": "markdown", "markdown": "Release checklist"}}. After that the tab and files.info both showed "Release checklist".
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Find a section and edit it
canvases.edit works on section IDs, and canvases.sections.lookup finds them. This is the script we ran:
import os
from slack_sdk import WebClient
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
CANVAS = "F0C5FC820DD"
found = client.canvases_sections_lookup(
canvas_id=CANVAS,
criteria={"section_types": ["h2"], "contains_text": "Steps"},
)
print("lookup:", found["sections"])
section_id = found["sections"][0]["id"]
resp = client.canvases_edit(
canvas_id=CANVAS,
changes=[{
"operation": "insert_after",
"section_id": section_id,
"document_content": {"type": "markdown", "markdown": "Owner: <@U0B7L4YK420>, due **Friday**\n"},
}],
)
print("edit:", resp["ok"])
Output:
lookup: [{'id': 'temp:C:fFTaa2fcad6da73f73f95545f589'}]
edit: True
The edit returned ok, but the mention did not work. In a canvas, <@U0B7L4YK420> is shown as typed. We replaced the line with the canvas form , and Slack showed a user chip:
What we learned about sections and edits:
- • Every list item, table cell and paragraph is its own section.
contains_text: "Numbered"returned two IDs, one per numbered item. - •
section_types: ["any_header"]returned the two headings: {"ok": true, "sections": [{"id": "temp:C:fFTc6bb24f0fb5082b28ea5ba58a"}, {"id": "temp:C:fFTaa2fcad6da73f73f95545f589"}]}. - • An empty
criteriaobject returnsinvalid_argumentswith[ERROR] must have minimum 1 properties [json-pointer:/criteria]. - •
replaceon the ID of the table cell "Ben" changed only that cell. The table stayed a table. - • Two changes in one
canvases.editcall returned[ERROR] no more than 1 items allowed [json-pointer:/changes]. Send one change per call.
Read the canvas back
files.info with the canvas ID returned "filetype": "quip", "mimetype": "application/vnd.slack-docs" and "pretty_type": "Canvas". A GET on its url_private_download, with the bot token in the Authorization header, returned text/html, not markdown. The section IDs from the lookup are the element IDs in that HTML:
<h2 id="temp:C:fFTaa2fcad6da73f73f95545f589">Steps</h2><p id="temp:C:fFT2e289a04b273e9b1f1ce392e8" class="line">Inserted after the Steps heading.</p>
The mention comes back as <a>@U0B7L4YK420</a>, a user ID with no name. To turn the canvas back into markdown, parse that HTML yourself. The script in export a Slack canvas does that conversion, and the same page covers the Print canvas route to PDF.
For the canvas editor's own shortcuts, which differ from the API's markdown, see Slack canvas formatting. For tables typed by hand, see how to make a table in Slack.
FAQ
Can the canvas API post Block Kit?
No. We sent canvases.edit with "document_content": {"type": "blocks", ...} and got invalid_arguments with [ERROR] failed to match exactly one allowed schema [json-pointer:/changes/0/document_content]. Markdown is the only content type. Use a message for Block Kit.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
How to See All Your DMs in Slack, Including the Hidden Ones
The DMs tab lists your direct messages with people. App DMs, Slackbot and DMs with deleted accounts sit elsewhere. We compared the tab with the full list the Slack API returned.
Slack Images Not Showing? 5 Causes We Reproduced
We uploaded images to a Slack test channel and then switched each image setting off, ran /collapse, and posted from a bot that was not in the channel. Five causes, each reproduced.
How to Message Yourself on Slack (and What It Notifies)
Your own name under Direct messages opens a private DM with yourself. We sent it a note, a file, a scheduled message and a reminder, and checked which ones raised a badge.