Slack Canvas Markdown: What Renders, What Fails, What Drops
A technical reference on what Slack Canvas markdown renders, what it quietly drops, and what causes hard API failures during document migration.
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 Markdown: What Renders, What Fails, What Drops
Slack Canvas accepts markdown through the document_content object in its API, but only a specific subset actually renders. The type field must be "markdown" — it is the only supported content type for the Web API. If you are converting documents from Quip, Confluence, Notion, or any other source, the gap between what Canvas supports and what it silently ignores (or outright rejects) determines whether your migrated content arrives intact.
This reference covers the exact supported set, the hard failures, the silent drops, and the converter policies that prevent data loss at scale.
Last verified: June 2025 against Slack API documentation for canvases.create, canvases.edit, conversations.canvases.create, and canvases.getContent.
Scope: This reference covers the Web API
document_content.markdownpath. If you're creating canvases through the Deno Slack SDK's built-in functions, Slack uses anexpanded_rich_textpayload instead of the markdown string covered here. (docs.slack.dev)Syntax note: Code examples throughout this document show Canvas API payload syntax. The callout and column fenced-div blocks (
::: {.callout},::: {.column}) are the syntax submitted in API payloads. Styled callout boxes in this blog post are rendered by the blog's own CMS and are not Canvas API syntax.
What markdown does Slack Canvas actually support?
Slack Canvas markdown is the subset of markdown Slack accepts in the document_content.markdown field when you create or edit a canvas. Anything not on the list below is either silently dropped or triggers a hard API failure. (docs.slack.dev)
| Category | Supported Elements |
|---|---|
| Text formatting | Bold, italic, strikethrough, inline code span |
| Headings | h1, h2, h3 only — nothing below h3 |
| Lists | Bulleted lists, ordered lists, checklists (task lists) |
| Block-level | Blockquote, callout, code block, divider (horizontal rule), paragraph |
| Layout | Column layout — max 3 columns |
| Tables | Markdown tables — max 300 cells per table |
| Links & mentions | Inline links, link references, @mentions for users and channels |
| Unfurls | Canvas unfurl, file unfurl, message unfurl, user unfurl, website unfurl |
| Other | Emojis (standard and custom), hard line breaks |
This list comes from Slack's developer documentation for canvases.create, canvases.edit, and conversations.canvases.create. If your source documents contain HTML blocks, footnotes, definition lists, or any standard markdown elements outside this set, they will not render. You must intercept them during conversion and rewrite them into supported equivalents — or mark them explicitly as unsupported.
Payload size limit: The
document_contentpayload is capped at 1 MiB (1,048,576 characters) per call. Forcanvases.edit, this limit applies to each individual change object in thechangesarray, not the total canvas size. Thechangesarray is currently limited to one operation per API call. Documents exceeding 1 MiB must be split across multiple API calls. (docs.slack.dev)
A minimal safe subset for an automated converter:
# Runbook
## Owners

### Checklist
- [ ] Export source
- [ ] Upload images
- [x] Validate links
> Freeze window starts at 18:00 UTC.
| Item | Owner |
| --- | --- |
| Prod cutover |  |
| Notes | **Read** `/ops/runbook` first |
Why h4, h5, and h6 headings don't exist in Canvas
Slack Canvas supports headings h1 through h3 only. The API does not return an error for deeper heading levels — the text is inserted but renders as a plain paragraph with no semantic heading role. For a single document, that is a minor annoyance. Across ten thousand converted documents, it destroys navigational structure.
The canvases.sections.lookup API confirms this constraint: you can filter sections by h1, h2, h3, or any_header. No deeper heading type exists in the Canvas section taxonomy. (docs.slack.dev)
Roundtrip behavior: If you write an h4 element and read it back with canvases.getContent, the API returns it as a plain paragraph — the heading markup is gone. This means roundtrip comparison (write → read → diff) will flag h4+ headings as conversion loss, which is the correct behavior: the content that went in is not the content that came out.
Options for flattening a deep heading tree
When a source document has four or five heading levels, you have several realistic strategies:
| Strategy | Mechanism | Best for |
|---|---|---|
| Collapse to three levels | Map h1→h1, h2→h2, h3/h4/h5/h6→h3 | Documents where deeper levels rarely stand alone |
| Promote and bold | Map top three levels to h1–h3, convert h4+ to bold paragraph text | Enterprise migrations; most reliable default |
| Split into multiple canvases | Break document at h3 boundaries; link via canvas unfurls | Documents genuinely requiring 5 levels; operationally expensive |
| Convert to tables | If h4 sections contain keyed values (owner, status, date), use a two-column table | Reference docs with structured sub-sections |
| Convert to callouts | Replace emphasis-as-hierarchy with native callout blocks | Deep headings standing in for emphasis rather than structure |
The wrong option is to pass the markdown through unmodified. An h4 rendered as body text is invisible to anyone scanning by headings, and Canvas provides no table-of-contents or outline view that would surface the problem.
Warning: If legal, regulatory, or SOP content depends on a deep outline tree, Slack Canvas is a lossy target. Make that determination before the migration, not during the final import pass.
What fails hard in Slack Canvas markdown?
A hard failure is content Slack rejects outright instead of silently simplifying. The distinction matters operationally: silent drops let the API call succeed while losing content; hard failures reject the entire payload, blocking canvas creation for every document containing the offending structure.
Block Kit is not supported inside canvases
Slack's documentation states this directly: "Note that currently, Block Kit is not supported in canvases." This is a hard boundary, not a soft limitation. (docs.slack.dev)
Block Kit powers interactive components in Slack messages — buttons, select menus, date pickers, overflow menus, and input blocks. None of these survive conversion into a canvas. The Canvas markdown format has zero representation for interactive UI beyond checklists.
This matters for migrations from tools like Quip, where spreadsheet-backed live components, Salesforce record links, and interactive widgets are embedded in documents. Attempting to inject Block Kit JSON into a Canvas payload results in a hard API failure. These elements must be converted to static text, markdown tables, or links to the external system — or flagged for human review.
If the source depends on interactivity, remap it before conversion. Convert the interaction to plain text instructions, move it to a workflow or modal, or fail the document for manual review. Do not pretend a button became content just because some label text made it through.
Nested structures that Canvas rejects outright
Some markdown structures that are valid in CommonMark or GFM will cause the Canvas API to reject the entire payload. The most documented failure is nesting a list inside a blockquote.
The error returned by both canvases.create and canvases.edit:
{
"ok": false,
"error": "canvas_editing_failed",
"detail": "'content' error: line 28: Unsupported block type (List) within block quote"
}This is not a silent drop — the entire API call fails, not just the offending line. A single deeply-nested list in a blockquote blocks creation of the entire canvas.
Nesting rules, based on both official documentation and observed behavior:
| Container | Allowed inside | Not allowed inside |
|---|---|---|
| Blockquote | Plain text paragraphs, inline formatting (bold, italic, inline code, links) | Headings, lists, code blocks, nested quotes |
| List items | Plain text, inline formatting | Code blocks, blockquotes, headings |
| Code blocks | Top-level only | Inside blockquotes or list items |
| Callouts | Plain text, inline formatting | Tables, nested structural elements |
| Column layouts | Plain text, inline formatting | Tables, callouts; max 3 columns |
A converter must pre-process the markdown to flatten nested structures before submitting to the API. The safest rewrites:
- Keep the quote, but collapse the list to plain quoted lines (manually prepending
•characters). - Keep the list, but hoist it outside the quote.
- Turn the quote into a callout, then follow it with a normal list.
Leaving this to Slack's parser is how you end up with rejected payloads in the middle of a bulk run.
The canvases.edit change object structure
The blog's earlier sections mention the changes array limit, but the structure of a change object is necessary to build against the edit API. A change object in the changes array has the following shape:
{
"operation": "insert",
"document_content": {
"type": "markdown",
"markdown": "## New Section\n\nContent here."
}
}Valid operation values are insert, replace, and delete. For replace and delete, a section_id must be provided to target a specific section. Section IDs are returned by canvases.sections.lookup.
{
"operation": "replace",
"section_id": "temp:C:abc123def456",
"document_content": {
"type": "markdown",
"markdown": "## Updated Section\n\nReplaced content."
}
}One operation per API call is the current constraint. A pipeline that needs to make five edits to a single canvas must make five sequential canvases.edit calls. At Tier 3 limits (50+ requests/minute), that is manageable — but the one-at-a-time constraint rules out batching changes for a single document in a single request.
Canvas access model: workspace-level vs. channel-linked canvases
Canvas access depends on how the canvas was created — a distinction that determines who can read and edit it.
canvases.create(nochannel_id): Creates a standalone canvas owned by the authenticated user. Access is private by default. The canvas must be explicitly shared or linked to a channel before other users can see it. On free Slack plans,canvases.createrequires achannel_id— standalone creation is a paid-plan feature.conversations.canvases.create(channel_idrequired): Creates a canvas linked to a specific channel. All members of that channel can view the canvas. Channel settings (private vs. public) determine who is a member.- Edit permissions: By default, any channel member can edit a channel-linked canvas. The canvas owner can restrict editing. API-created canvases inherit these defaults.
Migration implication: If you migrate 10,000 documents using canvases.create without channel linkage, every canvas is private by default. Users will not be able to find their content. Decide channel-linking strategy before the migration run, not after — retroactively linking canvases to channels requires additional API calls and may not reproduce the original document's sharing model accurately.
How images work in Canvas markdown
Canvas images are URL references, not binary payloads. The standard markdown image syntax ! [alt](url) works, but the URL must resolve to either a publicly reachable image URL or a Slack-hosted permalink. There is no place in the markdown payload to embed base64 or raw image bytes. (docs.slack.dev)
If the source image already has a stable public URL, use normal markdown syntax:
For private or migrated images, the required upload flow is:
- Call
files.getUploadURLExternalto request a secure upload destination. - POST the binary file data to the returned upload URL.
- Call
files.completeUploadExternalto finalize the file in the Slack workspace. - Call
files.infousing the returned file ID to extract thepermalink. - Insert the permalink into your Canvas markdown as
! [alt text](permalink).
# Step 1: Request upload URL
url_resp = client.files_getUploadURLExternal(
filename="diagram.png",
length=file_size
)
# Step 2: POST binary data to the returned upload URL
# (upload to url_resp["upload_url"] via HTTP PUT/POST)
# Step 3: Finalize the upload
complete_resp = client.files_completeUploadExternal(
files=[{"id": url_resp["file_id"]}]
)
# Step 4: Read back the permalink
info_resp = client.files_info(file=url_resp["file_id"])
permalink = info_resp["file"]["permalink"]
# Step 5: Use in canvas markdown
markdown = f""Access nuance: If you call files.completeUploadExternal without a channel_id, the file is uploaded but remains private — it has not been shared anywhere. The permalink is a Slack-authenticated URL; users without an active session matching the file context will see a broken image. Validate file visibility deliberately instead of assuming every Slack-hosted image resolves for every viewer. (docs.slack.dev)
Roundtrip behavior for images: If you write ! [alt](permalink) and read back with canvases.getContent, the image markdown is returned intact. Images do not get silently dropped on write — they either succeed (if the URL is reachable) or render as broken images in the canvas UI.
Scale planning: Every embedded image requires a separate upload-then-reference cycle. Tier 2 methods run at 20+ requests/minute. A document set with 5,000 images requires at minimum ~250 minutes for image uploads alone, before a single canvas is created. Plan the image pipeline as a separate, parallelized step with its own retry budget.
Canvas table constraints
Markdown tables in Slack Canvas are limited to 300 cells total per table, where cells = rows × columns.
| Columns | Max rows |
|---|---|
| 3 | 100 |
| 5 | 60 |
| 10 | 30 |
| 15 | 20 |
Table cells support inline formatting: bold, italic, strikethrough, inline code, links, mentions, checkboxes, ordered and unordered lists, and unfurls. They do not support block-level elements like code blocks or nested tables.
Roundtrip behavior for oversized tables: Submitting a table exceeding 300 cells results in a hard API failure, not a silent truncation. The error message specifies the cell count violation. If your source documents contain data tables with hundreds of rows, they must be split or converted (linked spreadsheet, attached CSV) before migration.
Callout and column layout syntax
Callouts and column layouts use a fenced-div syntax specific to Slack Canvas. This syntax is submitted in the document_content.markdown field of API payloads. It is not part of standard CommonMark or GFM.
Callout syntax (API payload):
::: {.callout}
This is important information.
:::Callouts render as colored blocks in the Canvas UI. They cannot contain tables and cannot be nested within other structural elements. The exact specification for color variants and icon customization is not fully documented in the Canvas API reference — color and icon behavior varies by Slack client version.
Column layout syntax (API payload):
::: {.column}
First column content
:::
::: {.column}
Second column content
:::Maximum 3 columns per layout. Tables and callouts are not supported inside column layouts.
What canvases.getContent returns for these elements: Callouts and columns are returned using the same fenced-div syntax on readback, making them roundtrip-safe. This is confirmed by the Slack documentation statement that getContent returns markdown in the same format accepted by create and edit. (docs.slack.dev)
Columns are layout, not document hierarchy. Use them for side-by-side content, not as a substitute for missing heading levels. (slack.com)
Unfurls: canvas, file, message, user, and website
Unfurls are Slack Canvas's way of embedding rich previews of other Slack objects inline. The five supported unfurl types:
| Unfurl type | Syntax | Render behavior |
|---|---|---|
| Canvas unfurl | ! [](canvas-url) |
Embedded canvas preview |
| File unfurl | ! [](slack-file-url) |
File metadata card |
| Message unfurl | ! [](slack-message-url) |
Message content preview |
| User unfurl (card) | ! [](@U123ABCDEFG) on its own line |
Profile card block |
| User mention (inline) | ! [](@U123ABCDEFG) within text |
Styled inline chip |
| Channel mention | ! [](#C123ABC456) |
Channel link chip |
| Website unfurl | External URL | Open Graph preview when resolvable |
The unfurl behavior is contextual — a user reference on its own line renders as a card; the same reference inline renders as styled text. The canvases.sections.lookup API exposes unfurl section types including canvas_unfurl, file_unfurl, message_unfurl, user_unfurl, and user_mention, confirming these are first-class constructs in the Canvas data model.
What should a converter do when there is no Canvas equivalent?
Every document migration hits constructs the target platform does not support. The question is what happens at that moment. There are three approaches, and the choice matters enormously at scale.
Decision table: converter policy by construct type
| Construct | Hard failure? | Recommended policy | Output |
|---|---|---|---|
| Block Kit interactivity | Yes | Fail loudly | Pipeline error + human review queue |
| Nested list in blockquote | Yes | Fail loudly or restructure | Hoist list; log transformation |
| h4+ headings | No (silent paragraph) | Degrade visibly | Bold text + migration marker |
| Images without stable URL | No (broken render) | Degrade visibly | Placeholder + upload task logged |
| Tables > 300 cells | Yes | Fail loudly or split | Split table + migration marker |
| Embedded spreadsheets | No equivalent | Degrade visibly | Link + migration marker |
| HTML blocks | No (silent drop) | Degrade visibly | Plain text + migration marker |
| Footnotes | No (silent drop) | Degrade visibly | Inline text + migration marker |
Fail loudly
Stop processing the document and log the unsupported construct with its location. This forces human review. Use this when the lost construct changes meaning: Block Kit interactivity, nested block structures that cause API rejections, or source embeds that are the only place important data lives.
Degrade with a visible marker
Replace the unsupported construct with its plain-text content, wrapped in a visible marker:
::: {.callout}
⚠️ MIGRATION NOTE: This document contained an interactive approval button that cannot be represented in Slack Canvas. Original label: "Approve Request"
:::This keeps the pipeline moving while leaving a searchable breadcrumb. After migration, search all canvases for MIGRATION NOTE and resolve them manually. This is the standard approach for enterprise migrations at scale — visible markers maintain user trust, explicitly state the boundaries of the migration, and provide a clear audit trail.
Silent degradation (the worst option)
Drop the unsupported element or convert it to unmarked plain text. The canvas looks fine at a glance, but the interactive widget, the embedded spreadsheet, the five-level heading tree — all gone without a trace.
Silent degradation is the default in most off-the-shelf conversion tools. At 50 documents, you might notice. At 10,000, you will not. You discover the missing content months later when someone searches for a specific approval workflow that no longer exists. By then, the source system may be decommissioned.
Build your converter to be loud about what it can't do. Every unsupported construct should produce either a pipeline error or a visible marker in the output.
How to validate a Canvas conversion pipeline
Validation means proving Slack stored what you intended, not just that the API returned ok: true. Create or edit the canvas, then immediately read it back with canvases.getContent as markdown and diff the returned content against your expected normalized output.
What canvases.getContent roundtrip actually tells you:
| Element written | What getContent returns |
Lossless? |
|---|---|---|
| h1, h2, h3 | h1, h2, h3 | Yes |
| h4, h5, h6 | Plain paragraph | No — heading markup stripped |
Callout (::: {.callout}) |
::: {.callout} |
Yes |
Column layout (::: {.column}) |
::: {.column} |
Yes |
| Markdown table (≤300 cells) | Markdown table | Yes |
| Image with valid permalink | Image markdown | Yes |
| Nested list in blockquote | API rejection (no canvas created) | N/A |
| Bold, italic, inline code | Bold, italic, inline code | Yes |
| Block Kit JSON | API rejection | N/A |
Roundtrip validation is not a complete QA strategy on its own. It catches structural losses (h4 → paragraph) and API rejections, but it does not validate semantic correctness — whether the migrated content means the same thing it meant in the source system. Pair roundtrip diffing with human spot-checks on a representative sample (typically 2–5% of documents) and automated checks for migration markers.
Round-trip every new mapping rule. If you add a transform for callouts, columns, deep headings, or image handling, push a sample canvas and read it back with canvases.getContent before rolling that rule into production.
API rate limits for Canvas operations
Canvas API methods are rate-limited per Slack's standard tier system. These are nominal limits; Slack's rate limiter uses a burst-and-throttle model, and sustained traffic above the nominal rate will receive HTTP 429 responses with a Retry-After header specifying the wait in seconds.
| Method | Tier | Nominal rate |
|---|---|---|
canvases.create |
Tier 2 | ~20 requests/minute |
conversations.canvases.create |
Tier 2 | ~20 requests/minute |
canvases.edit |
Tier 3 | ~50 requests/minute |
canvases.sections.lookup |
Tier 3 | ~50 requests/minute |
canvases.getContent |
Tier 3 | ~50 requests/minute |
files.getUploadURLExternal |
Tier 4 | ~100 requests/minute |
files.completeUploadExternal |
Tier 4 | ~100 requests/minute |
Retry strategy for 429 responses: Read the Retry-After header value (in seconds) and wait that duration before retrying. Do not implement fixed-delay retries — they will not match Slack's dynamic throttle windows. Exponential backoff without the Retry-After header will over-wait and under-utilize your rate limit budget.
Scale math for a 1,000-canvas migration:
- Canvas creation at ~20/min: minimum 50 minutes for document creation
- 5,000 images at ~20/min (
files.getUploadURLExternalis Tier 4, butcanvases.createis Tier 2): image uploads are not the bottleneck — canvas creation is - Post-write validation at ~50/min (
canvases.getContent): minimum 20 minutes for validation pass - Total minimum wall time for 1,000 documents with 5 images each: ~70 minutes at full rate limit, with zero retries and zero errors
Real migrations run at 40–60% of theoretical throughput due to errors, retries, payload splitting, and pipeline coordination overhead. Plan for 2–3× the theoretical minimum.
The sfdc_record_mention section type
The canvases.sections.lookup API exposes a section type called sfdc_record_mention that does not appear in the document_content supported elements list. Based on available documentation and observed behavior:
sfdc_record_mentionsections appear in the Canvas data model when a canvas contains embedded Salesforce record references.- These are listed on docs.slack.dev in the context of Slack for Salesforce integration, separate from standard Canvas API documentation.
- Creating an
sfdc_record_mentionvia the public Canvas API is not documented. It is likely generated by Slack's Salesforce integration layer rather than by direct API payload construction. - Workspaces without the Slack for Salesforce integration installed cannot create or meaningfully render these sections.
Migration implication for Quip documents: Quip documents that contain embedded Salesforce record links (CRM records, opportunity links) will not have a direct Canvas API equivalent for the embedded record card. The conservative approach is to convert these to plain hyperlinks pointing to the Salesforce record URL and add a migration marker. Do not assume the sfdc_record_mention section type is writable via canvases.create without testing against your specific workspace's integration state.
Quip-to-Canvas element mapping
| Quip Element | Canvas Equivalent | Fidelity | Notes |
|---|---|---|---|
| Headings h1–h3 | h1–h3 | Full | Direct mapping |
| Headings h4+ | None (paragraph) | Loss | Flatten or restructure; roundtrip drops heading |
| Bold, italic, strikethrough | Bold, italic, strikethrough | Full | Direct mapping |
| Bullet/numbered lists | Bullet/ordered lists | Full | Direct mapping |
| Checklists | Checklists | Full | Direct mapping |
| Code blocks | Code blocks | Full | Top-level only; cannot be nested |
| Images | Image via URL | Partial | Requires upload + permalink cycle |
| Spreadsheets (small) | Markdown table (≤300 cells) | Partial | Oversized sheets need splitting |
| Spreadsheets (large) | Linked file or CSV | Loss | No native equivalent |
| @mentions | @mentions | Partial | Syntax differs; IDs must be remapped |
| Embedded Salesforce records | sfdc record unfurl (integration-dependent) | Partial | Requires Slack for Salesforce; fallback to hyperlink |
| Interactive buttons/forms | None | Loss | No Canvas equivalent; fail loudly |
| Embedded Quip docs | Canvas unfurl (if migrated) | Partial | Requires all referenced docs migrated first |
| HTML blocks | None | Loss | Silently dropped; degrade with marker |
| Footnotes | None | Loss | Silently dropped; convert to inline text |
Where documentation ends and observed behavior begins
Slack's official documentation for canvases.create and canvases.edit lists the supported markdown elements explicitly. The callout and column layout fenced-div syntax (::: {.callout}, ::: {.column}) is less well-documented — it appears in SDK references, MCP server implementations, and community tooling rather than in the primary Canvas API docs.
The canvases.sections.lookup API reveals section types like chart, citation, and sfdc_record_mention that are not listed in the document_content supported elements. These represent internal or feature-flagged constructs that exist in the Canvas data model but are not available for creation via the public API.
Behaviors observed that are not explicitly documented:
- h4+ elements do not error on write — content is silently inserted as a paragraph, and
canvases.getContentreturns it as a paragraph. The heading markup is gone. - The callout fenced-div syntax works reliably in practice, and roundtrips correctly through
getContent. Color options and icon customization are not fully specified. - Table cell formatting is richer than the docs suggest — checkboxes, lists, and unfurls inside cells work. The boundary of what fails in cells is not fully specified; test before relying on it in production.
- Nested list in blockquote errors are whole-payload failures — not line-level. One offending structure blocks the entire canvas.
This document does not speculate about future Slack roadmap items. The supported set documented today is the set to build against.
The bottom line for migration teams
Slack Canvas is a constrained document target, not a generic markdown renderer. It handles the fundamentals well — headings up to h3, lists, tables up to 300 cells, inline formatting, code blocks — but has hard limits on nesting depth, heading levels, interactivity, image handling, and document size that will break assumptions built on richer source formats.
The four things that catch teams off guard every time:
- Heading levels stop at h3. h4+ is silently converted to a plain paragraph on write, and the heading is gone on readback. Every source document with deeper structure needs pre-processing.
- Nested structures inside blockquotes cause hard API failures. One list inside a blockquote blocks creation of the entire canvas — not just that section.
- Images require a separate upload cycle. There is no way to inline binary image data in the markdown payload. Image uploads must be pre-staged before canvas creation begins.
- Canvas access is private by default. Canvases created without
channel_idlinkage are invisible to other users until explicitly shared. Decide the sharing model before the migration run.
Knowing the supported set precisely — and building your converter to be explicit about what falls outside it — is the difference between a migration that works and one that looks like it works until someone needs the content that got silently dropped.
Frequently Asked Questions
- What markdown elements does Slack Canvas support?
- Slack Canvas supports headings h1–h3, bold, italic, strikethrough, inline code, code blocks, bulleted/ordered/checklists, blockquotes, callouts, dividers, column layouts (max 3), markdown tables (max 300 cells), inline links, @mentions, emojis, and five unfurl types (canvas, file, message, user, website). The document_content payload is capped at 1 MiB per call.
- Can you use Block Kit inside a Slack Canvas?
- No. Slack's documentation explicitly states that Block Kit is not supported in canvases. Interactive elements like buttons, select menus, date pickers, and input blocks have no representation in Canvas markdown. Attempting to inject Block Kit JSON into a Canvas payload will cause a hard API failure.
- How do you embed images in a Slack Canvas via the API?
- Images require either a publicly reachable URL or a Slack-hosted permalink. For private images, upload the file using files.getUploadURLExternal and files.completeUploadExternal, then retrieve the permalink via files.info and reference it in your markdown as . Base64 or binary image data cannot be embedded in the markdown payload.
- What heading levels does Slack Canvas support?
- Slack Canvas supports h1, h2, and h3 only. Heading levels h4, h5, and h6 are silently rendered as plain paragraph text with no error from the API. Source documents with deeper heading hierarchies must be flattened before conversion.
- Why does the Canvas API fail with 'Unsupported block type (List) within block quote'?
- Slack Canvas does not support nested lists inside blockquotes. The API rejects the entire payload — not just the offending line. You need to flatten the structure by hoisting lists out of blockquotes or converting list items to plain quoted text before submitting the markdown.