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

How to Validate a Slack Canvas Migration: Reconciliation Guide

A 200 OK from the Slack Canvas API doesn't mean your content arrived intact. Learn how to validate a canvas migration with reconciliation checks, not assumptions.

Roopendra Talekar Roopendra Talekar · · 19 min read
How to Validate a Slack Canvas Migration: Reconciliation Guide
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

How to Validate a Slack Canvas Migration: Reconciliation Guide

A migration is not finished when the last API call returns 200 OK. It is finished when you can prove — with evidence, not inference — that every document arrived, arrived complete, and is accessible to the right people. For Slack Canvas migrations, this proof is harder to produce than expected, because the Canvas API is designed to accept your input politely and tell you very little about what it actually stored.

This guide covers the full validation pipeline: building a source inventory, confirming arrival in Slack, comparing body lengths and element counts, verifying the access grant matrix, sampling for content fidelity, and packaging results into a repeatable reconciliation report. If you are migrating from Quip, pair this with our Quip to Slack Canvases migration guide for the extraction side. For the broader migration process, see our data migration playbook.

Danger

An unvalidated migration is an assumed migration. The honest deliverable is a reconciliation report that documents what arrived, what didn't, and what was accepted as-is — not a declaration that it worked.

Canvas Type Definitions

Before running any validation, establish which canvas type each document maps to:

  • Standalone canvas: Created via canvases.create. Has its own canvas_id (an F-prefixed file ID). Shared explicitly with channels or users via canvases.access.set. Access is independent of channel membership.
  • Channel canvas: Created via conversations.canvases.create and attached directly to a channel. Accessible to all channel members by default. Visible in channel.properties.canvas from conversations.info. Cannot be reshared independently.

The validation logic differs between these types — standalone canvases require explicit access grant verification, while channel canvases require channel-attachment verification. Confusing the two produces false passes in your reconciliation report.

Why a 200 Response Does Not Prove a Successful Migration

A 200 OK from the Slack Canvas API confirms the server accepted your request. It does not confirm the content was stored as you sent it. Three failure modes produce a successful HTTP response while silently degrading the migrated content.

1. Silent content truncation at the 1 MiB boundary. The canvases.edit method accepts markdown content up to 1 MiB (1,048,576 characters) per document_content object, and Slack documents that only one edit operation is supported per API call. (docs.slack.dev) When migrating complex documents, your extraction scripts often sit behind middleware or transformation logic that parses HTML, Markdown, or proprietary block formats into Slack's markdown equivalents. If your transformation layer encounters an unexpected node type — a nested iframe from Confluence, a custom Salesforce macro in Quip — it may drop the node entirely to keep the rest of the payload valid. Slack receives the sanitized, partial payload, accepts it, and returns {"ok": true}.

For documents that exceed 1 MiB, your splitting logic must create multiple chunks. If the splitting function has an off-by-one error, the first chunk arrives truncated and the second starts at the wrong offset. The result: a canvas with a hole in the middle that looks fine unless you read it.

2. Table rows lost to the 300-cell ceiling. Canvas tables enforce a hard limit of 300 cells per table — any combination of rows and columns that adds up to that number. (docs.slack.dev) A 10-column table can hold 30 rows (including the header). A 4-column table can hold 75. If your source document has a 10-column table with 50 rows (500 cells), the Canvas API will accept the table but render only the cells that fit within the 300-cell budget. The extra 200 cells vanish. No error. No warning.

3. Partial access grants that succeed for some channels and fail for the rest. The canvases.access.set method accepts at most 20 channel_ids or 20 user_ids per request. (docs.slack.dev) That forces batching. If you are granting access to 50 channels per canvas and a failure occurs in the second or third batch, the canvas exists, is readable by some channels, and is invisible to the rest. The API returns errors per call, not per channel_id within a call.

When a batch contains a mix of regular channel IDs and DM/MPDM channel IDs, Slack rejects the entire call — not just the offending IDs. The response returns {"ok": false, "error": "invalid_channel"} for the whole batch, meaning all valid channel IDs in that batch also fail silently. If your error handling treats "the first batch call succeeded" as "all calls succeeded," you have an access gap with no per-ID signal to diagnose it. The only recovery path is to re-submit the batch after stripping DM/MPDM IDs.

These three failure modes share one trait: the migration script finishes without errors. The only way to catch them is to go back and read what you wrote.

How to Build the Source Inventory Before You Extract Anything

A source inventory is a complete manifest of every document, its metadata, and its structural properties, captured from the source system before extraction begins. It is the baseline against which every later check is compared. If you skip this step, you have nothing to compare against, and your validation is just "does the canvas exist? Yes? Ship it."

Capture this per document in your source system (Quip, Confluence, Notion, or whatever you are migrating from):

  • Document ID in the source system
  • Title
  • Body length in characters (after converting to the markdown format you will send to Slack)
  • Body hash (SHA-256 of the normalized markdown — see normalization spec below)
  • Table count and cell count per table (rows × columns)
  • Image/embed count
  • Target channel IDs — every channel this document should be shared with after migration
  • Expected canvas type — standalone or channel canvas
  • Access level per channel — read or write
  • Exception flags — documents intentionally split, flattened, skipped, or manually repaired

Store this as a JSON or CSV file with one row per document. This file is your ground truth.

{
  "source_id": "quip_abc123",
  "title": "Q3 Incident Runbook",
  "body_length_chars": 48210,
  "body_hash": "3f5d...a8c2",
  "tables": [
    {"rows": 12, "columns": 4, "cells": 48},
    {"rows": 8, "columns": 6, "cells": 48}
  ],
  "image_count": 3,
  "target_channels": [
    {"channel_id": "C01ABC", "access_level": "write"},
    {"channel_id": "C02DEF", "access_level": "read"},
    {"channel_id": "C03GHI", "access_level": "read"}
  ],
  "canvas_type": "standalone"
}

Normalization Specification

Define expected length as the character count of the normalized markdown you intend to load after mapping, cleanup, and chunking — not the raw source length. Apply the following transformations before hashing or measuring, identically on both the source side (pre-migration) and the target side (post-retrieval via canvases.getContent):

  1. Strip UTF-8 BOM (\xef\xbb\xbf) if present
  2. Normalize line endings to \n (convert \r\n and \r to \n)
  3. Strip trailing whitespace per line (spaces and tabs at end of each line before \n)
  4. Collapse multiple consecutive blank lines to a single blank line
  5. Strip leading and trailing whitespace from the entire document
  6. Encode as UTF-8 before hashing

Document these rules in your migration README. If two implementations of this normalization produce different hashes for the same input, they are not equivalent. The hash is only comparable when both sides apply identical transformations.

Length comparison catches missing content fast. A hash catches reordering or character substitution when the length happens to match. Both checks together confirm arrival and integrity.

If you are using Slack's official Quip conversion flow, note that converted Quip documents become read-only in Quip, which is another reason to freeze your baseline before starting. (slack.com)

How to List All Canvases in Slack (There Is No canvases.list)

Slack does not provide a canvases.list endpoint. To enumerate canvases programmatically, use files.list with the types parameter set to canvas. This is the only supported way to get a list of canvases via the API, which is why indexing canvases after migration is a manual process. (docs.slack.dev)

curl -s "https://slack.com/api/files.list?types=canvas&count=100" \
  -H "Authorization: Bearer $SLACK_TOKEN" | jq '.files[] | {id, title, created}'

files.list is a Tier 3 method (50+ requests per minute). It returns paginated results — the default page size is 100. Paginate through the entire result set using the page parameter to build a complete target inventory. A one-page check will undercount any non-trivial migration.

The response includes the file ID (which is the canvas ID — it starts with F), the title, creation timestamp, and other file metadata. It does not include the canvas body. To get the body, call canvases.getContent per canvas.

Rate Limit Budget for Validation Runs

files.list and canvases.getContent are both Tier 3 (50+ requests/minute). The table below shows estimated minimum validation time at the sustained rate limit, accounting only for canvases.getContent calls (one per canvas):

Canvas count Minimum time at Tier 3 (50 req/min)
100 ~2 minutes
500 ~10 minutes
1,000 ~20 minutes
5,000 ~100 minutes (~1.7 hours)
10,000 ~200 minutes (~3.3 hours)

Add files.list pagination time on top: at 100 canvases per page, 10,000 canvases require 100 files.list calls (~2 additional minutes). For large migrations, schedule validation windows during off-peak hours and implement exponential backoff for HTTP 429 responses.

Building the Target Inventory

For each canvas returned by files.list, call canvases.getContent to retrieve the full content as markdown:

import hashlib
 
def normalize(text):
    """Apply canonical normalization before hashing or length comparison."""
    if text.startswith('\xef\xbb\xbf'):
        text = text[3:]                          # strip UTF-8 BOM
    text = text.replace('\r\n', '\n').replace('\r', '\n')  # normalize line endings
    lines = [line.rstrip() for line in text.split('\n')]   # strip trailing whitespace per line
    text = '\n'.join(lines)
    import re
    text = re.sub(r'\n{3,}', '\n\n', text)      # collapse multiple blank lines
    return text.strip()
 
def build_target_inventory(canvas_ids, client):
    inventory = []
    for cid in canvas_ids:
        resp = client.canvases_getContent(canvas_id=cid, content_type="markdown")
        if resp["ok"]:
            content = resp["content"]
            normalized = normalize(content)
            inventory.append({
                "canvas_id": cid,
                "body_length_chars": len(normalized),
                "body_hash": hashlib.sha256(normalized.encode('utf-8')).hexdigest(),
                "table_count": content.count("\n|---"),  # rough heuristic; see sections.lookup below
                "image_count": content.count("![](http"),  # count external image URLs only
            })
    return inventory

canvases.getContent returns the entire canvas in a single content string with no pagination. It accepts markdown or html as the content_type. Use markdown for length and hash comparison against your source inventory; use HTML if you need to parse structured elements more precisely. (docs.slack.dev)

Warning

Image count caveat: Slack's canvas markdown uses ! [](http... syntax for actual external images, but uses ! [](#CHANNEL_ID) for channel references and ! [](@USER_ID) for user mentions. Counting every ! []( token overcounts. The safer heuristic — content.count("! [](http") — counts only tokens with an absolute URL. For a precise count, compare against an image manifest captured during migration rather than relying on any regex heuristic. (docs.slack.dev)

The Canvas Reconciliation Protocol: Five Checks

The following five checks form The Canvas Reconciliation Protocol, a structured validation framework covering both completeness (did it arrive?) and fidelity (did it arrive correctly?). Counting handles completeness. Reading handles fidelity. You need both.

Check 1: Document Count — Source vs Target

The most basic check: compare the number of documents in your source inventory to the number of canvases returned by files.list?types=canvas. If the counts do not match, identify the missing IDs by diffing the source document ID list against your source-to-canvas mapping table.

A count match is necessary but not sufficient. It tells you the right number of canvases exist. It says nothing about whether they contain the right content.

Match on stable identifiers, not titles. Two canvases can share a title. Validation needs a persistent mapping from source document ID to Slack canvas ID, maintained during migration.

For channel canvases, add a second cross-check. conversations.canvases.create returns a canvas ID, and the canvas can also be found in channel.properties.canvas via conversations.info. (docs.slack.dev) This verifies not just that a canvas exists, but that it is attached to the correct channel.

Check 2: Body Length and Hash Per Document

For each canvas, compare the character count and SHA-256 hash from canvases.getContent (after applying the normalization spec above) against the expected values in your source inventory.

An exact match after normalization is the ideal outcome but is not always achieved. Slack's markdown parser applies its own heading syntax normalization, list formatting adjustments, and whitespace handling that may differ subtly from the normalization applied to the source. Use the following thresholds as a starting calibration point, then adjust based on your specific pipeline's observed behavior during a pilot run of 20–50 documents before full-scale validation:

Deviation Interpretation Action
0–2% shorter Normal formatting normalization Pass
2–10% shorter Possible content loss; review likely Flag for sampling
>10% shorter Probable content loss Fail — remediate
Any longer than source Likely content duplication from retry Fail — investigate
Hash mismatch, length match Reordering or character substitution Flag for sampling

These thresholds are a heuristic starting point derived from common pipeline behavior, not empirical constants. Calibrate them against your specific source format (Quip, Confluence, Notion) and transformation logic before treating them as pass/fail criteria.

For documents that crossed the 1 MiB per-chunk limit, calculate the expected full-body hash from the reassembled target markdown in final order, then compare that to the markdown returned by canvases.getContent.

Check 3: Table and Image Counts Per Canvas

Count tables and images in each retrieved canvas body and compare against the source inventory.

For tables, use canvases.sections.lookup to count table sections precisely rather than relying on the markdown heuristic. The method accepts a canvas_id and a section_types filter. Pass ["table"] to retrieve only table sections:

def count_tables_via_api(canvas_id, client):
    resp = client.canvases_sections_lookup(
        canvas_id=canvas_id,
        criteria={"section_types": ["table"]}
    )
    if resp["ok"]:
        return len(resp["sections"])
    return None

Each section object in the response includes a section_id and type metadata. To count cells per table, retrieve the section content and parse the markdown table within it. Compare each table's expected cell count to the target. A table that was 500 cells in the source (10 columns × 50 rows) becomes a 300-cell table in Slack — 30 rows instead of 50. Flag any table where source_cells > 300 as a known data-loss candidate and verify the actual row count in the migrated canvas. (docs.slack.dev)

For images, count against an image manifest captured during migration, then verify that each expected image reference appears in the canvas body. Check that images actually resolve, not just that references are present. If media is part of the scope, see our guide to migrating images and attachments for why image references break.

Warning

Table cell limits are absolute. Do not attempt to bypass the 300-cell limit by nesting tables or using unsupported block types. If a source table exceeds this limit, your migration playbook must dictate a fallback before cutover — split into multiple tables, convert to CSV via files.upload_v2 and embed the file link, or log it as an accepted exception. Do not discover that policy during QA.

Check 4: The Access Grant Matrix

A grant matrix is the full expected set of {canvas_id, channel_id, access_level} relationships after migration. Validation passes only when every required row exists, not when the canvas merely exists.

Note: a standalone canvas can be shared with up to 1,000 channels. If any document in your inventory approaches this ceiling, flag it before migration — there is no API workaround for the limit, and it is not surfaced as an error during migration if the canvas accumulates shares incrementally.

The Canvas API does not provide a "list who has access to this canvas" endpoint. Use three complementary approaches:

Option A: Probe from the channel's perspective. For each channel that should have access, call canvases.getContent using a bot token that is a member of that channel but has no other access path to the canvas. If the call succeeds, the channel grant is working. If it returns canvas_not_found, the grant is missing. Note that canvases.getContent returns canvas_not_found both when the canvas does not exist and when the caller cannot view it. (docs.slack.dev)

Option B: Check file sharing metadata. files.info responses include sharing-related data such as shares, channels, groups, and ims. Your validator app needs files:read plus canvases:read scopes. (docs.slack.dev)

Option C: Audit migration logs. During migration, log every canvases.access.set call with its full request payload and response. During validation, parse the logs to find any call that returned an error, and cross-reference against the expected grant matrix. Given that a mixed-type batch (regular + DM channel IDs) causes entire-call rejection, your logs should show the full rejected payload — use this to identify which regular channel IDs were collateral failures and requeue them.

Use multiple approaches: Option C to quickly find known failures, Option A or B to catch failures that were not logged.

For channel canvases, access is tied to channel membership, so the validation target is: did the correct channel get the correct canvas attachment? Use the conversations.info cross-check described in Check 1.

For standalone canvases, also verify that each canvas has the correct owner and that the access level (read vs write) matches the intended permission. A migration that grants read when the intent was write leaves users unable to edit documents they previously maintained.

If you are on Enterprise Grid, Slack's Audit Logs API includes canvas actions such as canvas_access_added, canvas_access_upgraded, canvas_access_downgraded, and canvas_access_revoked. Treat these as supplementary evidence, not as a replacement for direct reconciliation. (docs.slack.dev)

Warning

DMs and MPDMs cannot receive canvas access via canvases.access.set. Only regular channel IDs are accepted. When a batch includes a DM or MPDM channel ID, Slack rejects the entire batch with {"ok": false, "error": "invalid_channel"} — the valid channel IDs in that batch also fail. Scrub all DM/MPDM IDs from batches before submission and resubmit the valid IDs separately.

Check 5: Exception Output Queue

If any check fails, the script flags the document ID, records the specific variance (e.g., "Image count mismatch: Expected 4, Found 2"), and outputs it to an exception queue for remediation or explicit acceptance.

Exception Triage: When to Remediate vs Accept

Not every discrepancy warrants a fix. Use this decision framework for each flagged item:

Remediate if:

  • Body length deviation exceeds 10% shorter (probable content loss)
  • A table that should have ≤300 cells is missing rows that were not expected to be truncated
  • An access grant is missing for an active channel (not archived)
  • Image references appear in the canvas but return 404 when resolved
  • A document that should have arrived is completely absent

Accept if:

  • A table exceeding 300 cells in the source was split into multiple tables per documented migration policy and all data is present in the split tables
  • Content contains platform-specific elements Slack Canvases do not support: inline LaTeX (rendered as plain text), Quip Salesforce integration macros, Confluence page properties macros
  • A canvas was intentionally not shared with an archived channel
  • A Quip document is among the approximately 35% incompatible with direct conversion — Salesforce's FAQ indicates only roughly 65% of Quip documents are compatible with Slack Canvas (help.salesforce.com)
  • A Quip comment-only permission type has no equivalent in Slack Canvases and was downgraded to read access per documented policy

Escalate if:

  • You cannot determine whether missing content is a transformation bug or a platform limitation
  • A document is present but unreadable by anyone, including the owner
  • Body hash mismatches cannot be explained by normalization differences

Why Counting Is Not Enough: Sampling for Content Fidelity

Content fidelity means the migrated canvas reads the same as the source document — not just that it has roughly the same character count. Counts prove arrival. Only reading proves content.

A 100% read-through is impractical for large migrations. Sample strategically:

  • All documents that failed a quantitative check (length deviation beyond threshold, missing tables, missing images). These are confirmed problems; read them to assess severity.
  • All documents with tables exceeding 300 cells in the source. These will have lost rows. Read them to confirm the truncation is where you expect and that the split (if any) was applied correctly.
  • Every document near the 1 MiB chunk boundary. These are the most likely to have splitting errors.
  • The 10 largest documents by character count. Long documents accumulate more conversion edge cases.
  • A random 5–10% of documents that passed all quantitative checks. This catches formatting corruption, broken links, garbled markdown, and other fidelity issues that do not change the character count.

For each sampled document, compare the migrated canvas (via canvases.getContent with content_type="html" for easier visual diffing) against the source. Check for:

  • Heading structure and order preserved
  • List ordering (numbered lists especially — Slack markdown can renumber)
  • Table content and row order correct, including first and last rows
  • Image references that actually load, not just present as markup
  • Links pointing to the right targets (not to source-system internal URLs)
  • Code blocks preserved with correct language tags
  • @user mentions mapped to Slack user IDs, not appearing as dead text or raw email addresses
  • Section boundaries for chunked documents — confirm no gap between the end of chunk N and the start of chunk N+1
Tip

Use counts for completeness and sampling for fidelity. If you only sample, you can miss silent loss at scale. If you only count, you can miss content that technically arrived but no longer says the same thing. The Canvas Reconciliation Protocol requires both.

How to Make the Validation Run Repeatable

A validation run should be a script, not a manual process. It should produce the same output given the same state. This matters because you will run it at least twice: once after the initial migration, and again after remediation.

1. Parameterize the run. Accept the source inventory file and a run ID as inputs. Output a timestamped report tied to that run ID. Never overwrite a previous report.

2. Separate data collection from comparison. The script should have two phases: (a) collect the target inventory from Slack, (b) compare against the source inventory and produce the reconciliation report. This lets you re-run comparisons against cached data without hitting the API again.

3. Store raw API responses. Save the full canvases.getContent response for every canvas during the collection phase. If a dispute arises later ("this document was fine on Tuesday"), you have the receipts.

4. Make the comparison deterministic. Sort both inventories by document ID before comparing. Apply the normalization spec identically on both sides. Document the normalization rules in the script's README and enforce them with a test against a known input/output pair.

# validation_run.py --source inventory.json --run-id 2026-10-02-initial
 
import json, os, datetime
 
def run_validation(source_path, run_id, client):
    source = json.load(open(source_path))
    target = collect_target_inventory(client)  # calls files.list + getContent
 
    report = {
        "run_id": run_id,
        "timestamp": datetime.datetime.utcnow().isoformat(),
        "source_count": len(source),
        "target_count": len(target),
        "exceptions": [],
        "passed": [],
        "needs_review": []
    }
 
    # ... comparison logic per checks 1-5 ...
 
    os.makedirs(f"reports/{run_id}", exist_ok=True)
    json.dump(report, open(f"reports/{run_id}/reconciliation.json", "w"), indent=2)

After a remediation pass — re-migrating failed documents, patching truncated content, fixing access grants — run the same script with a new run ID. Diff the two reports to confirm that previously failing documents now pass and that remediation did not introduce new failures.

Tip

Naming convention for reports: Use {run_id}_reconciliation.json with the run ID encoding the date and purpose: 2026-10-02-initial, 2026-10-05-post-remediation, 2026-10-07-final. This makes it trivial to find the right report when debugging issues months later.

How to Record Accepted Exceptions

The reconciliation report must distinguish between three states:

Status Meaning
Pass Source and target match within tolerance on all checks
Fail A discrepancy exists that requires remediation
Accepted Exception A discrepancy exists, is documented, and has been explicitly approved by the migration stakeholder

For each accepted exception, record the document ID, the check that failed, the expected and actual values, who approved the exception and when, and the reason:

{
  "document_id": "quip_xyz789",
  "check": "table_cell_count",
  "expected": 500,
  "actual": 300,
  "status": "accepted_exception",
  "approved_by": "j.martinez@acme.com",
  "approved_at": "2026-10-01T14:30:00Z",
  "reason": "Table split into two canvases per migration policy. All 500 cells preserved across splits. Stakeholder approved new layout."
}

This matters because six months from now, someone will find a canvas with missing rows and open a ticket. The accepted exception log is the artifact that proves it was a known trade-off, not a missed bug.

If you are using Salesforce's official Quip-to-Slack conversion, the exception register matters even more: Salesforce's FAQ indicates that only roughly 65% of Quip documents are compatible with Slack Canvas, and some permission types — including Quip comment-only access — do not map cleanly. (help.salesforce.com) Those are exception candidates if the business accepts them, not silent passes.

The Reconciliation Report: Your Actual Deliverable

The deliverable of a migration is not "we ran the script and it finished." The deliverable is a reconciliation report that answers five questions:

  1. How many documents were in scope? (Source inventory count)
  2. How many arrived? (Target inventory count, with IDs of any missing)
  3. How many arrived complete? (Body length, hash, table count, image count within tolerance)
  4. How many are accessible to the right people? (Access grant matrix verification)
  5. What exceptions were accepted and by whom? (Exception log with approvals)

At minimum, include: source document count, expected canvas count, actual canvas count, missing canvases, unexpected canvases, body length and hash mismatches (with deviation percentages), table and image mismatches, grant matrix mismatches, fidelity sample results, accepted exceptions with approvals, remediation queue, and rerun summary after fixes.

This report is the migration's paper trail. When a stakeholder asks "did the migration work?", the honest answer is never just "yes" or "no." It is "here is exactly what happened, with evidence."

If your migration vendor or internal team cannot produce this report, the migration was not validated. It was assumed.

Slack Canvas API Constraints Reference

A quick reference for the API limits that shape your validation logic:

Constraint Value Source
canvases.edit content limit 1 MiB (1,048,576 chars) per document_content Slack docs
Canvas table cell limit 300 cells per table (any row/column combination) Slack docs
canvases.create rate limit Tier 2: 20+ per minute Slack docs
canvases.edit rate limit Tier 3: 50+ per minute Slack docs
canvases.getContent rate limit Tier 3: 50+ per minute Slack docs
files.list rate limit Tier 3: 50+ per minute Slack docs
canvases.access.set per-call limit 20 channel_ids or user_ids Slack docs
canvases.access.set mixed-type batch behavior Entire batch rejected on first invalid channel ID Slack docs
canvases.access.set channel restriction Regular channels only — DM/MPDM IDs cause full-batch rejection Slack docs
Canvas sharing ceiling Up to 1,000 channels per canvas Slack help
canvases.getContent pagination None — returns entire canvas in one response Slack docs
Quip document compatibility with Slack Canvas ~65% compatible per Salesforce FAQ Salesforce help

What Actually Proves the Migration Worked

The migration script's job is to move data. The validation script's job is to prove the data moved correctly. These are two different scripts with two different concerns, and they should never be conflated.

Run The Canvas Reconciliation Protocol against your source inventory. Fix the failures. Run it again. Record the exceptions you choose not to fix, with documented approvals. Produce the reconciliation report. Hand it to the stakeholder. That report — not the migration script's exit code — is what proves the migration worked.

If you need a deeper testing framework, start with our migration playbook guide. If you are on the Salesforce path out of Quip, our Quip to Slack Canvases guide covers what the official conversion does and does not preserve.

Frequently Asked Questions

How do you list all canvases in a Slack workspace via the API?
There is no canvases.list method. Use files.list with types=canvas to enumerate all canvases. It returns paginated results at Tier 3 rate limits (50+ requests/minute). Each result includes the canvas/file ID, title, and creation timestamp, but not the body content — that requires a separate canvases.getContent call per canvas.
Is a 200 response from canvases.edit enough to prove migration success?
No. A 200 OK only confirms Slack accepted a syntactically valid request. If your pipeline truncated the document to fit the 1 MiB limit, dropped table rows to stay under the 300-cell ceiling, or failed an access grant batch, Slack still returns success for the calls it did process.
What is the table cell limit in Slack canvases?
Slack canvases enforce a hard limit of 300 cells per table — any combination of rows and columns that adds up to 300. A 10-column table can hold 30 rows. Cells beyond 300 are dropped without an error.
How do you verify canvas access permissions after migration?
The Canvas API has no 'list access' endpoint. Verify grants by probing with canvases.getContent from a bot with only channel-level access, checking files.info sharing metadata, or auditing your migration logs for failed canvases.access.set calls. Only regular channel IDs work — DM and MPDM IDs are rejected.
What should a Slack Canvas reconciliation report include?
At minimum: expected versus actual canvas counts, missing and extra canvases, body length and hash mismatches, table and image count mismatches, grant matrix mismatches, fidelity sample results, accepted exceptions with approvals, remediation queue, and rerun summary after fixes.

More from our Blog