Back to Blog
Guide

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.

Slack Green Team
October 1, 2026
October 1, 2026
3 min read
Share:
slack api
developers
canvas

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

CallTokenResponse
canvases.createbot{"ok": false, "error": "free_teams_cannot_create_standalone_canvases"}
canvases.createuser{"ok": false, "error": "free_teams_cannot_create_standalone_canvases"}
conversations.canvases.createbot{"ok": true, "canvas_id": "F0C5FC820DD"}
conversations.canvases.create, same channel againbot{"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.

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: ![](@U0B7L4YK420) and channel ![](#C0C5QG3TQKV)

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 canvas in Slack: an empty

The rest also worked: the ![](@U...) 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 bottom of the canvas: a quote, a code block,

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 ![](@U0B7L4YK420), and Slack showed a user chip:

Two canvas screenshots. With the user mention written in message syntax the canvas shows the raw user ID text. With the canvas mention syntax it shows an @sieun 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 criteria object returns invalid_arguments with [ERROR] must have minimum 1 properties [json-pointer:/criteria].
  • • replace on the ID of the table cell "Ben" changed only that cell. The table stayed a table.
  • • Two changes in one canvases.edit call 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.

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.

Always Active

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

Guide

Slack Interactivity Payload: Real block_actions JSON and response_url Limits

We clicked a button, a static_select and a datepicker in Slack on 1 October 2026 and logged the block_actions payloads our app received. Then we measured response_url: 5 posts, then used_url; it worked at 29:50 and was dead at 30:10.

Slack Green Team
Guide

Slack unfurl_links and unfurl_media: What Each Flag Does, Tested

We posted a web page, a YouTube video and an image with every combination of unfurl_links and unfurl_media on 1 October 2026, then built a custom preview with link_shared and chat.unfurl. The flags do not split the way the names suggest.

Slack Green Team
Guide

Slack Scheduled Message Didn't Send? 5 Cases We Tested

We scheduled five messages for 4:40 PM on 1 October 2026, then archived, left and deleted their channels and deleted a thread parent before the send time. Two sent, three never did, and Slack gave no notice about the three.

Slack Green Team