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

Quip to Slack Canvas Migration at Scale: API Limits & Scripting Guide

The native Quip-to-Slack converter handles one doc at a time. At scale, you need custom scripts against both APIs — this guide covers real rate limits, payload ceilings, and pipeline architecture.

Roopendra Talekar Roopendra Talekar · · 21 min read
Quip to Slack Canvas Migration at Scale: API Limits & Scripting 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

Quip to Slack Canvas Migration at Scale: API Limits & Scripting Guide

The native Quip-to-Slack-Canvas converter works one document at a time and does not support bulk conversion. If your Quip workspace has more than a few hundred documents, the only viable migration path is custom scripting — extract via the Quip API, transform the HTML to Slack's markdown format, and write to Slack using the canvas creation endpoints. There is no off-the-shelf tool that handles this end-to-end. (slack.com)

This guide covers the real API constraints on both sides, the throughput math, the scripting architecture you need, and the validation strategy that proves nothing got dropped or duplicated.

Danger

Quip End-of-Life (March 2027): Salesforce is retiring all Quip products. Subscriptions cannot be renewed after March 1, 2027. Free accounts lose access on March 31, 2027. After your subscription expires, Quip enters a three-phase shutdown: read-only, blocked logins, then data deletion. Start your migration now — API-based extraction at scale takes longer than most teams expect.

Why the Native Converter Does Not Work at Scale

Salesforce's official Quip-to-Slack conversion is an in-product flow — users click "Convert to a Slack Canvas" on individual documents inside the Quip UI. It is not an API, it is not scriptable, and bulk document conversion is not supported. The original Quip document becomes read-only after conversion. (slack.com)

For a team with 50 documents, clicking through one at a time is tedious but workable. For a workspace with 5,000 or 50,000 documents, it is not a migration plan. The native path also cannot handle spreadsheets with Salesforce Data Mentions, embedded Live Apps, or documents with complex nested tables — all of which need transformation logic that a simple UI converter cannot provide.

This is strictly custom scripting work. There is no vendor product that maps Quip's data model to Slack Canvas. You are responsible for extracting the data, transforming the markup, handling network failures, and proving every document made it across.

For a walkthrough of what the native flow preserves and what it drops, see Quip to Slack Canvases Migration: The Official Salesforce Path.

What Are the Quip API Rate Limits?

Quip exposes two API surfaces for extraction: the Automation API (user-level) and the Admin API (site-wide). A complete migration typically needs both.

Automation API (per-user):

  • 50 requests per minute per user token
  • 750 requests per hour per user token
  • Company-wide cap: 600 requests per minute across all users and integrations

Admin API (per-admin):

  • 100 requests per minute per admin token
  • 1,500 requests per hour per admin token

Bulk Export API:

  • 36,000 documents exported per hour per company
  • Async operation: you submit thread IDs, receive a request_id, then poll for results
  • Exports to PDF, DOCX, XLSX, or HTML — not markdown

The Bulk Export API provides dedicated rate-limit headers: X-Documentbulkexport-RateLimit-Remaining tells you how many documents you can still request in the current window, and X-Documentbulkexport-Retry-After tells you how many seconds to wait if you have exceeded the cap. (quip.com)

For document content you intend to transform, the Automation API's Get Thread Html V2 endpoint returns structured HTML and paginates with cursor and next_cursor, which matters for very large documents.

When to Use Admin API vs. Automation API

Use this decision framework before writing any extraction code:

Condition Use Admin API Use Automation API
Need all workspace threads, including restricted docs ✓ —
Token owner has admin role in Salesforce org ✓ —
Extracting only documents visible to a specific user — ✓
Plan does not include Quip Plus or Advanced — ✓ (only option)
Need thread metadata (owner, created date, folder membership) ✓ Partial
Extracting HTML for transformation — ✓ (Get Thread Html V2)
Enumerating all threads for inventory ✓ (/1/admin/threads/list) —

The practical default for bulk migration: Use the Admin API for inventory (Phase 1) and the Automation API with admin credentials for HTML extraction (Phase 2). The Admin API's /1/admin/threads/list sees all threads regardless of document-level permissions. A single user's Automation API token will silently miss documents that the token owner cannot access — a common source of incomplete migrations that only surfaces in the count validation step.

The HTTP 503 Trap

Warning

Quip returns HTTP 503 for rate limits, not 429. This is documented behavior that breaks standard retry logic. Most HTTP client libraries treat 503 as a transient server error and retry aggressively — exactly the wrong response to a rate limit. Quip does not consistently send a Retry-After header on 503 responses.

When you exceed Quip's rate limits, the API returns an HTTP 503 with the body "Over Rate Limit" instead of the standard HTTP 429. Standard HTTP client libraries — like Python's requests with a default urllib3 Retry object — treat 503 as a fatal server error or a temporary gateway issue. If your retry logic does not explicitly distinguish rate-limit 503s from real 503s, your script will either crash or trigger retry storms that make the situation worse.

Your Quip API client must:

  1. Inspect the 503 response body — if it contains "Over Rate Limit", treat it as a throttling event, not a server error.
  2. Read the X-Ratelimit-Reset header if present — this gives the UTC timestamp when the rate-limit window resets.
  3. Fall back to exponential backoff — start at 5 seconds, double each retry, cap at 120 seconds.
  4. Separate rate-limit 503s from real 503s — a genuine server error should be logged and alerted differently. Without this distinction, your monitoring generates noise that obscures real failures.
def classify_quip_response(status, headers, body=""):
    """Quip returns 503 for rate limits, not 429."""
    if status == 503 and (
        'X-Company-Retry-After' in headers
        or 'X-Documentbulkexport-Retry-After' in headers
        or 'X-Ratelimit-Reset' in headers
        or 'over rate limit' in body.lower()
    ):
        return 'rate_limited'
    if status == 429:
        return 'rate_limited'
    if status >= 500:
        return 'server_error'
    return 'other'

Plan Requirements: Quip Plus or Advanced

The Admin API and Bulk Export API are not available on all plans. Quip's documentation states that API access requires Quip Plus, Quip Advanced, a pre-existing Quip for Customer 360 product, or Lightning Experience in Enterprise, Professional, Performance, Unlimited, or Developer editions. If your company is on a basic or free Quip plan, you are limited to user-level Automation API extraction at 50 requests per minute. (quip.com)

Entitlement details can be inconsistent across Salesforce's documentation — treat this as a scoping risk and verify API enablement before you commit to dates.

What Are the Slack Canvas API Limits?

Slack provides two methods for creating canvases programmatically:

  • canvases.create — creates a standalone canvas owned by the acting user or bot
  • conversations.canvases.create — creates the channel canvas for a specific channel

Both are Tier 2 in Slack's rate-limit system: a guaranteed minimum of 20 requests per minute per app per workspace. Slack allows occasional bursts above 20 but does not publish exact burst ceilings — in practice, Tier 2 methods sustain roughly 20–30 requests per minute in steady-state workloads before 429s appear. For editing existing canvases, canvases.edit is Tier 3 at 50+ requests per minute, though only one change is supported per call. (docs.slack.dev)

Constraint Limit Notes
canvases.create rate Tier 2: ~20–30/min per app per workspace Burst ceiling unpublished; plan for 20/min sustained
conversations.canvases.create rate Tier 2: ~20–30/min per app per workspace Same tier as canvases.create
canvases.edit rate Tier 3: 50+/min per app per workspace One change per call
canvases.getContent rate Tier 3: 50+/min per app per workspace Returns full canvas, no pagination
document_content max size 1 MiB (1,048,576 characters) Per object, measured in characters not bytes
Table cell limit 300 cells per table Any combination of rows × columns
Supported content type Markdown only Block Kit not supported in canvases
Channel canvas uniqueness One per channel Returns channel_canvas_already_exists if one exists

Slack returns HTTP 429 with a Retry-After header when rate-limited — the standard behavior that Quip notably does not follow.

For most bulk migrations, standalone canvases are the safer default. Channel canvases are unique per channel, so they work if your migration maps exactly one document per channel. For everything else, standalone canvases give you flexibility — you can always tab a standalone canvas into a channel using channel_id.

The 1 MiB Ceiling

The 1 MiB limit on the document_content markdown payload is measured in characters, not bytes. Most rich-text Quip documents land well under 100 KB when converted to markdown. But spreadsheet-heavy Quip threads with hundreds of rows can blow past 1 MiB once serialized to a markdown table.

When a document exceeds the limit:

  1. Split and append — create the canvas with the first chunk, then use canvases.edit with insert_at_end to append additional content (each edit is also capped at 1 MiB per change).
  2. Split the table — if the oversize content is a single large table, break it into multiple smaller tables within the canvas.
  3. Archive as a file — for truly enormous spreadsheets, create the canvas with a summary and attach the full dataset as a linked file.

Note that canvases.edit can return canvas_too_large, so whole-canvas growth still needs guardrails even after successful creation. Slack does not publish a total canvas size ceiling. (docs.slack.dev)

The 300-Cell Table Limit

Slack canvas tables are capped at 300 cells per table — any combination of rows and columns that totals 300. A 10-column Quip spreadsheet can hold at most 30 rows (including the header row) in a single canvas table. Your transformation script must detect tables exceeding 300 cells and choose one of the following strategies:

Strategy When to Use Trade-off
Split into continuation tables with repeated headers Table must remain queryable Increases canvas length; headers duplicated
Convert to fenced code block (CSV format) Data is reference-only Loses formatting; not visually scannable
Upload as file attachment with canvas summary Table >1,000 cells or >10 columns Best fidelity; requires file upload step

Transformation Fidelity Matrix

Every Quip content element requires an explicit decision during transformation. Elements with no Slack Canvas equivalent must be serialized to a fallback format or flagged for manual review.

Quip Element Slack Canvas Equivalent Transformation Rule Fidelity
<h1> – <h3> headings #, ##, ### Direct mapping Full
<h4> – <h6> headings ### (clamped) Clamp to level 3 Partial — hierarchy lost
Ordered list <ol> 1. numbered list Direct mapping Full
Unordered list <ul> - bullet list Direct mapping Full
Checklist (unchecked) - [ ] Parse Quip CSS class q-checked=false Full
Checklist (checked) - [x] Parse Quip CSS class q-checked=true Full
Bold <strong> **text** Direct mapping Full
Italic <em> _text_ Direct mapping Full
Inline code <code> `text` Direct mapping Full
Code block <pre> ```text``` Direct mapping Full
Hyperlink <a href> [text](url) Rewrite internal Quip URLs (two-pass) Full after Pass 2
HTML table ≤300 cells Pipe-delimited markdown table Split if >300 cells Full if within limit
HTML table >300 cells Multiple tables or code block Apply split strategy Partial
Inline image ! [alt](slack-url) Download blob, re-upload to Slack Full after re-host
File attachment Slack file permalink Download via /1/blob, re-upload Full after re-host
User mention (@name) <@SLACK_USER_ID> Requires Quip→Slack user ID mapping table Full if mapping exists
Salesforce Data Mention Static text (display value) Serialize cell display value; no live data Degraded — static only
Quip Live App (e.g., Poll) None Drop or append [Live App: {type} not migrated] None
Formula cell Static text (computed value) Serialize to display value Degraded — static only
Document comment / annotation Appended text block or drop Convert to > blockquote or discard Partial
Salesforce record embed Static text or link Serialize as [Salesforce: {object} {id}] Degraded
Conditional formatting None Strip; preserve cell value None
Section divider <hr> --- Direct mapping Full

Elements marked Degraded or None should be logged in the payload change log delivered to business owners after migration.

Throughput Math: How Long Will Your Migration Take?

The bottleneck is almost always the Slack write side.

Quip extraction rate (Automation API):

  • 50 docs/min per user token, capped at 600/min company-wide
  • With 4 parallel user tokens: ~200 docs/min (well within the 600/min company cap)
  • 10,000 documents ≈ 50 minutes of extraction

Slack write rate:

  • canvases.create at ~20/min per app per workspace (sustained; plan conservatively)
  • 10,000 documents ≈ 500 minutes (≈ 8.3 hours) with a single app
  • With 3 Slack apps writing in parallel: ≈ 2.8 hours

The Slack side is roughly 10× slower than the Quip side. For a workspace with 10,000 documents, expect the write phase alone to take 3–9 hours depending on parallelism.

These numbers assume one API call per document. In practice, throughput depends on calls per document — a large document that needs cursor-paginated extraction and multi-chunk canvas creation consumes more API calls than a simple one-pager. Run a pilot on a representative sample of 50–100 documents, measure actual calls per document, and use that figure in your forecast before committing to dates.

A useful planning formula:

docs_per_hour ≈ min(
  36000 / bulk_export_docs_per_doc,
  (750 * quip_users) / quip_html_calls_per_doc,
  1200 / slack_create_calls_per_doc,
  3000 / slack_edit_calls_per_doc
)

Example calculation for a 10,000-document workspace:

  • Assume 1.2 Quip HTML calls per doc (some paginate), 1.5 Slack calls per doc (some chunk)
  • Quip extraction ceiling: (750 × 4 users) / 1.2 = 2,500 docs/hr
  • Slack write ceiling (single app): 1,200 / 1.5 = 800 docs/hr
  • Binding constraint: Slack write at 800 docs/hr → 12.5 hours for 10,000 docs
  • With 3 parallel Slack apps: ~4.2 hours

These are calculated estimates. Actual runs vary based on document complexity, network latency, and burst behavior. Teams migrating 10,000+ documents should treat 4–6 hours (3 apps) as a planning baseline, not a guarantee.

Tip

Separate extraction and loading. Run Quip extraction as a standalone phase that writes all documents to local storage. Then run the Slack write phase against the local cache. This decouples the two rate-limit regimes and lets you re-run either side independently.

For workspaces above 30,000 documents, even parallel Slack apps will push the migration past a full business day. Plan for overnight or weekend execution windows with checkpointing to survive interruptions.

The Pipeline Architecture

The migration breaks into five phases. Every phase reads from and writes to the same manifest or state database, giving you full resumability and an audit trail.

Phase 1: Inventory

Use the Admin API's List Company Threads endpoint (GET /1/admin/threads/list) to enumerate every thread in your Quip workspace. This returns thread IDs, document types (document, spreadsheet, chat), titles, last-modified timestamps, and owner information. Export this to a manifest file or database — it drives every subsequent step.

Filter out chat threads and decide separately how to handle spreadsheets before you write anything to Slack. (quip.com)

Important: A single Quip thread can appear in multiple folders simultaneously. Quip folders behave more like tags than a strict hierarchy — the same document can have multiple parent folder IDs. If your migration maps Quip folders to Slack channels, you must decide before Phase 1 how to handle multi-folder documents:

  • Canonical location: Pick one folder as primary (e.g., the oldest, or the one closest to the root) and migrate there only.
  • Copy to all locations: Create separate canvas copies per channel. Requires deduplication logic to avoid treating copies as missing originals.
  • Ignore folder structure: Migrate all canvases to a flat workspace and rely on search.

Document this decision in your manifest schema before you start — it cannot be changed mid-migration without reprocessing.

Phase 2: Extract

For each thread in the manifest, call GET /1/threads/{thread_id} via the Automation API to retrieve the document's HTML body. The API returns structured HTML preserving headings, lists, tables, and inline formatting. Large documents may paginate via cursor and next_cursor, so extraction must be cursor-aware. Store each document locally:

{
  "quip_thread_id": "ABCDEF12345",
  "title": "Q3 Account Plan",
  "html": "<h1>Q3 Account Plan</h1><p>...</p>",
  "extracted_at": "2026-09-15T14:30:00Z",
  "slack_canvas_id": null
}

Optionally generate bulk export sidecars (HTML, PDF, DOCX, XLSX) for audit and fallback. Real migrations often use both: bulk export for coverage and audit, Get Thread Html V2 for transform-ready HTML.

Image blob extraction: For each inline image encountered during HTML parsing, download the binary asset using GET /1/blob/{thread_id}/{blob_hash}. This endpoint requires the same user-level OAuth token as the Automation API — no separate auth flow. The blob hash is embedded in the src attribute of <img> tags in the returned HTML. There is no published per-blob size limit in Quip's documentation, but blobs exceeding 1 GB are uncommon and should be flagged for manual review. Store downloaded blobs locally with their blob hash as filename before any Slack upload step.

Phase 3: Transform

Convert Quip HTML to Slack-compatible markdown. Apply the transformation fidelity matrix from the table above. Key implementation notes:

  • Headings: Clamp <h4>–<h6> to ###. Log every clamp so document owners know heading hierarchy changed.
  • Tables: Parse HTML <table> to pipe-delimited markdown. Before writing, count cells (rows × columns). If >300, apply the split strategy appropriate for that document type.
  • Checklists: Quip encodes checked state in CSS classes (q-checked="true" / q-checked="false"). Parse these attributes; do not rely on visual inspection of the HTML.
  • User mentions: Maintain a mapping table of Quip user IDs (from <a class="mention"> tags) to Slack member IDs. The official Quip-to-Slack conversion path matches by email address. Your custom pipeline must replicate this: call the Quip /1/users/{user_id} endpoint to get the email, then query the Slack users.lookupByEmail endpoint to get the Slack member ID.
  • Salesforce Data Mentions: These render as live data in Quip but have no equivalent in Slack Canvas. Serialize them as static text using the display value present in the HTML at extraction time. Log every instance.
  • Document size: Validate that the final markdown is under 1,048,576 characters. Apply the split strategy before calling any Slack endpoint.

Phase 4: Load

For each transformed document, call canvases.create (standalone) or conversations.canvases.create (channel-attached). Record the returned canvas_id back into your manifest.

import time
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
 
client = WebClient(token="xoxb-your-bot-token")
 
def create_with_backoff(title: str, markdown: str, max_retries: int = 5) -> str:
    for attempt in range(max_retries):
        try:
            response = client.canvases_create(
                title=title,
                document_content={"type": "markdown", "markdown": markdown}
            )
            return response["canvas_id"]
        except SlackApiError as e:
            if e.response.status_code == 429:
                retry_after = int(e.response.headers.get("Retry-After", 30))
                time.sleep(retry_after)
            else:
                raise
    raise RuntimeError(f"Failed after {max_retries} retries")

For documents exceeding 1 MiB, create the canvas with the first chunk, then append subsequent chunks using canvases.edit with insert_at_end.

Image uploads: For each downloaded Quip blob, call files.upload_v2 before canvases.create. The upload response includes a permalink field — substitute this URL into the markdown payload in place of the original Quip blob URL. files.upload_v2 is a Tier 2 method (~20/min); factor this into your throughput calculation for image-heavy workspaces.

Slack file storage quota: Slack enforces file storage limits by plan — Free workspaces have a 5 GB total limit, Pro and above have no published per-workspace cap but Enterprise Grid accounts are governed by org-level storage policies. Before migrating image-heavy workspaces, audit current Slack file storage usage via admin.analytics.getFile (Enterprise) or the Slack admin dashboard. A workspace with 10,000 documents averaging 5 images at 200 KB each adds ~10 GB of file storage. Confirm headroom before starting the load phase.

Phase 5: Validate

Compare total Quip threads in the inventory against documents marked as loaded in the manifest and total canvases enumerable via the Slack API. All three numbers should match. See the validation section below for the full strategy.

Preserving the interconnected web of documents is one of the hardest parts of a Quip migration. If you migrate text as-is, internal links point to dead Quip URLs after the 2027 shutdown.

You cannot fix links during initial creation because Document A might link to Document B, but Document B has not been migrated yet. The solution is a two-pass architecture:

Pass 1 (Creation): Iterate through all Quip documents, convert content, and push to Slack Canvases. Store the mapping of quip_thread_id → slack_canvas_id in your state database. Leave raw Quip URLs in the text.

Pass 2 (Update): Iterate through the newly created Slack Canvases. Search the markdown for any URL matching your Quip domain. Extract the Quip thread ID from the URL, query your state database for the corresponding Slack Canvas ID, and replace the URL. Push the updated markdown back to Slack using the canvases.edit endpoint.

This guarantees all internal links resolve correctly regardless of migration order. Links pointing to Quip threads that were not migrated (e.g., chat threads, excluded documents) should be replaced with a static label — [Quip link: not migrated] — rather than left as dead URLs.

Handling Embedded Images and Attachments

Quip documents frequently contain inline images and file attachments. These assets do not transfer when you push markdown to Slack, and Quip-hosted URLs will become permanently inaccessible after the shutdown.

Your script must:

  1. Download the blob from Quip using GET /1/blob/{thread_id}/{blob_hash}. Auth: same user OAuth token as Automation API. The blob hash appears in <img src> and <a href> attributes in the extracted HTML.
  2. Upload the file to Slack using the files.upload_v2 endpoint.
  3. Retrieve the Slack-hosted permalink from the upload response.
  4. Inject that new Slack URL into the markdown payload before calling canvases.create.

This adds significant time to the migration. Downloading and re-uploading large files introduces network latency and consumes Slack file storage quotas (see the storage note in Phase 4 above). Plan your concurrency model to handle binary transfers asynchronously so the main text conversion pipeline is not blocked.

For a deeper treatment of asset migration patterns, see How to Migrate Images, Attachments & Embeds Without Broken Links.

Checkpointing and Idempotency

Checkpointing means recording the state of the migration so a crashed or interrupted run can resume from where it stopped rather than starting over.

Back your migration with a state database. SQLite is sufficient for single-worker runs; use PostgreSQL for concurrent workers. A practical schema:

CREATE TABLE quip_to_slack_manifest (
  quip_thread_id TEXT PRIMARY KEY,
  quip_updated_usec BIGINT NOT NULL,
  target_surface TEXT NOT NULL,
  slack_channel_id TEXT NULL,
  slack_canvas_id TEXT NULL,
  html_cursor TEXT NULL,
  chunk_count INT DEFAULT 0,
  last_completed_chunk INT DEFAULT -1,
  source_hash TEXT NULL,
  target_hash TEXT NULL,
  state TEXT NOT NULL,           -- 'pending' | 'extracted' | 'transformed' | 'loaded' | 'validated' | 'failed'
  folder_ids TEXT NULL,          -- JSON array of all Quip folder IDs for this thread
  canonical_folder_id TEXT NULL, -- resolved single target folder after dedup policy
  last_error TEXT NULL
);

On restart, your script reads the manifest and skips any entry that has already been successfully loaded. If extraction or loading fails mid-document, you resume from the last saved cursor or chunk rather than recreating the canvas from scratch.

Idempotency ensures running the same script twice produces the same result — no duplicated canvases, no missing documents.

The Slack canvas API does not provide native idempotency keys. canvases.create will happily create a second canvas with the same title and content. Your idempotency must live in your own manifest:

  1. Before creating a canvas, check the manifest for an existing slack_canvas_id against that quip_thread_id. If one exists, skip.
  2. After creating a canvas, immediately write the slack_canvas_id to the manifest before processing the next document.
  3. Handle the crash window — if the script crashes between the API call and the manifest write, the next re-run creates a duplicate. Add a pre-run deduplication scan: list all canvases and reconcile against your manifest, flagging any orphans from a previous crash.

For channel canvases, Slack enforces uniqueness naturally: conversations.canvases.create returns channel_canvas_already_exists when a channel already has a canvas. Your script can catch this error and record the existing canvas ID via conversations.info. (docs.slack.dev)

def migrate_document(quip_thread_id, db_connection):
    cursor = db_connection.cursor()
    cursor.execute(
        "SELECT slack_canvas_id FROM quip_to_slack_manifest WHERE quip_thread_id = ?",
        (quip_thread_id,)
    )
    result = cursor.fetchone()
    
    if result and result[0]:
        print(f"Skipping {quip_thread_id}, already migrated to Canvas {result[0]}")
        return result[0]
        
    # Proceed with extraction and creation...

Validating the Migration

Post-migration validation answers one question: did every Quip document land as a Slack canvas with the correct content?

Count Validation

Compare three numbers:

  • Total Quip threads in inventory (from Phase 1)
  • Total documents marked loaded in the manifest
  • Total canvases in Slack (enumerate via files.list filtered to the canvas type)

All three should match. The equation source_docs = migrated + skipped + failed must balance. If it does not, the difference tells you exactly which documents failed, were skipped, or were duplicated.

Note that files.list returns all canvases accessible to the token, including canvases created outside the migration. Filter by created timestamp range to isolate migration output.

Content Validation

For critical documents, spot-check a random sample (5–10% or a fixed set of high-value documents):

  1. Fetch the canvas content via canvases.getContent, which returns the full canvas markdown in a single content string with no pagination. (docs.slack.dev)
  2. Compare a SHA-256 hash of the stored transform output against a hash of what Slack returns (after normalizing whitespace).
  3. Verify that heading count, table row count, checklist item count, and image reference count match the source document.
  4. For documents marked as split (chunk_count > 1), verify that all chunks are present by checking total character count.

Full content diffing is overkill for most migrations, but structural counts catch the most common failures: truncated content, dropped tables, and missing images without requiring manual review of every document.

What to Watch For

  • Silently truncated documents: If a document exceeded 1 MiB and your script did not split it, the API rejects the request — but only if you check the response. A swallowed error looks like a success in the manifest.
  • Missing images: Quip-hosted images disappear after shutdown. If you did not download and re-host them, canvases will have broken image links. Validate image URL count: compare <img> tags in source HTML against ! [ references in the target canvas.
  • Collapsed tables: A 500-cell table that was not split fails at the Slack API, but the error message may not be obvious. Log every table split operation and verify continuation tables appear in the canvas.
  • Heading hierarchy changes: Documents that used <h4>–<h6> will have different visual structure after clamping to ###. Include these in the payload change log.
  • Payload change log: Log every instance where a document was truncated, a table was converted to a fallback format, a Live App was dropped, or a Salesforce Data Mention was serialized as static text. Provide this log to business owners before sign-off.

Edge Cases and Failure Modes

Quip spreadsheets vs. documents: Quip spreadsheets are structurally different from documents. The Automation API returns spreadsheet content as HTML tables, but cell data may include formulas, Salesforce Data Mentions, and conditional formatting — none of which transfer to Slack Canvas. Your script must serialize spreadsheet cells to their display values, not formula strings.

Quip threads in multiple folders: A single Quip thread can exist in multiple folders simultaneously. Quip treats folders more like tags than a strict hierarchy. Apply the canonical location policy defined in Phase 1. Without this policy, the same document creates multiple canvases and the count validation will fail.

Quip chat threads: Chat threads (message-style threads attached to documents) do not have a direct equivalent in Slack Canvas. You can discard them, convert them to markdown within the canvas body as a > blockquote appendix, or post them as Slack messages linked to the canvas. Bulk export can include conversation history for DOCX and XLSX sidecars, so comments can also become plain-text appendices.

Token scope blindness: User-level Automation API tokens only see threads the token-generating user can access. If your workspace uses document-level permissions, a single user's token will miss restricted documents. Use the Admin API to enumerate all threads, then extract with admin-level credentials.

Identity mapping: The official Quip-to-Slack conversion path matches accounts by email. If you bypass that flow with a custom pipeline, build your own mapping: call /1/users/{user_id} on each Quip user ID encountered, extract the email field, then call Slack's users.lookupByEmail to get the Slack member ID. Build this mapping table once during Phase 1 before extraction begins. (help.salesforce.com)

When to Build This Yourself vs. Hire Help

This migration is feasible for an engineering team with Python or Node scripting experience and familiarity with API rate-limit management. If you have one engineer who can dedicate 2–3 weeks to building, testing, and running the pipeline, self-service is reasonable for workspaces under 5,000 documents.

Above 5,000 documents — or if your workspace includes spreadsheets with Salesforce data, complex permissioning, or embedded Live Apps — the edge cases multiply fast. The 503 rate-limit behavior, table-splitting logic, two-pass link rewriting, idempotency layer, and blob re-hosting each take non-trivial time to implement and debug. Teams that have done this before can scope these risks accurately; teams building the pipeline for the first time typically underestimate by 2–4×.

For a detailed guide on exporting your Quip data as a standalone step, see How to Export Data from Quip: Methods, API Limits & Migration Prep. For broader context on where to take your documents after Quip retires, see Where to Move Documents After Quip Retires: 2027 Decision Guide.

Frequently Asked Questions

Can you bulk convert Quip documents to Slack canvases?
No. Salesforce's native Quip-to-Slack converter works one document at a time. Bulk conversion is not supported. For workspaces above a few hundred documents, you need custom scripts using the Quip Automation API for extraction and the Slack canvases.create API for loading.
What are the Quip API rate limits for migration?
The Quip Automation API allows 50 requests per minute per user and 750 per hour per user, with a company-wide cap of 600 requests per minute. The Bulk Export API supports 36,000 documents per hour. Quip returns HTTP 503 (not 429) when rate-limited, so standard retry logic needs custom handling.
What are the Slack canvas size and table limits?
The Slack canvas API accepts a document_content object with a maximum of 1 MiB (1,048,576 characters) of markdown per request. Canvas tables are limited to 300 cells per table in any combination of rows and columns.
How long does a Quip to Slack Canvas migration take?
The Slack write side is the bottleneck at 20 canvas creations per minute per app. A 10,000-document workspace takes roughly 8 hours with one app, or about 3 hours with three parallel apps. Quip extraction is significantly faster at up to 600 requests per minute company-wide.
Why does Quip return 503 instead of 429 for rate limits?
This is a documented Quip API behavior. When you exceed rate limits, Quip returns HTTP 503 with an 'Over Rate Limit' message body instead of the standard HTTP 429 with a Retry-After header. Scripts must check the 503 response body and rate-limit headers to distinguish throttling from actual server errors.

More from our Blog