Slack Canvas to Box: The Complete Archival Guide
Moving Slack canvases to Box is archival, not migration. This guide covers inventory, export formats, folder mapping, permissions, rate limits, and metadata for compliance-driven teams.
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 to Box: The Complete Archival Guide
Moving Slack canvases into Box is not a migration — it is an archival operation. Slack Canvas is a live collaboration surface embedded in channels and DMs. Box is a governed file platform built around versions, retention policies, legal holds, security classifications, and metadata templates. The moment a canvas enters Box, it becomes a static file. In-place editing, real-time co-authoring, and Slack-native interactivity are gone. What you gain is governance infrastructure that Slack Canvas cannot provide: defensible retention schedules, legal holds that survive user deletion, metadata-driven classification, and eDiscovery integrations with tools like Relativity and Exterro.
This guide covers who should archive canvases into Box, who should not, the technical mechanics of inventory and export, folder construction, permission mapping, format trade-offs, token scope requirements, idempotency, failure recovery, and the rate-limit math that determines how long the whole operation takes.
Who Should Archive Slack Canvases into Box
Box's governance layer supports retention policies at the global, folder, or file level, legal holds that preserve content indefinitely for litigation, and eDiscovery partner integrations that enable automatic content export and review. Slack Canvas, by contrast, is a living document that multiple users can edit simultaneously, and its version history is limited — Slack does not preserve every edit the way Google Docs does. Canvases are also not included in standard Slack data exports, and many IT teams discover too late that they must be manually preserved before workspace shutdown.
Teams that should move canvases into Box:
- Regulated industries with SEC 17a-4, HIPAA, or FINRA obligations requiring immutable archival and defined retention periods
- Legal and compliance teams that need to place content under legal hold and export it through Box Governance's eDiscovery pipeline
- Organizations consolidating on Box as their single content layer, where canvases are orphaned documentation that needs a governed home
- Teams decommissioning a Slack workspace — if an acquisition, merger, or workspace sunset requires shutting down a Slack instance, canvas data must be extracted and stored before the workspace is permanently deleted
Teams that should not:
- Teams that actively edit canvases daily — moving them to Box kills the collaboration loop with no replacement
- Teams whose canvases are ephemeral (meeting notes, sprint retrospectives) with no retention requirement
- Organizations on Slack Enterprise Grid that already use the Discovery API for eDiscovery — legal holds, the Discovery API, and audit logs are only available on Enterprise Grid, so organizations already on that tier may be able to meet preservation obligations without moving content out of Slack at all
If your goal is to find a new live wiki or collaboration surface, Box is the wrong destination. For live collaboration alternatives, see our guides on Quip to Slack Canvases Migration or Where to Move Documents After Quip Retires.
Decision Framework: Archive or Not
Does the canvas contain content subject to a retention obligation?
├── Yes → Archive to Box
└── No → Is the canvas actively edited at least weekly?
├── Yes → Leave in Slack; do not archive
└── No → Is the workspace being decommissioned?
├── Yes → Archive to Box
└── No → Is eDiscovery coverage required?
├── Yes + Enterprise Grid → Use Discovery API; skip Box
├── Yes + non-Grid → Archive to Box
└── No → Discard or leave in Slack
Why This Is Archival, Not Migration
Archival means converting live, editable content into a static, governed record. Migration implies the destination preserves the source's working model. Box does not replicate Canvas's inline editing, checklist toggling, emoji reactions, or channel-scoped co-authoring. Calling this a migration sets the wrong expectation with stakeholders. Set it correctly from the start: the output is governed files, not collaborative documents. Use the word archive in the project plan and everyone — legal, IT, end users — will make better decisions about what belongs in Box and what does not.
Required API Credentials and OAuth Scopes
Before writing a line of code, define your token configuration. Missing a scope is the most common reason a first run produces an incomplete inventory.
Slack Scopes
| Scope | Purpose | Required or Optional |
|---|---|---|
files:read |
Read canvas content via files.list and canvases.getContent |
Required |
channels:read |
List public channels and their members | Required |
groups:read |
List private channels the token has access to | Required for private channels |
im:read |
List DM conversations containing canvases | Required for DM canvases |
mpim:read |
List group DM conversations containing canvases | Required for group DM canvases |
users:read |
Resolve <@U12345> user mentions to display names |
Required |
users:read.email |
Map Slack user IDs to email addresses for Box user lookup | Required for permission mapping |
audit:read |
Export access logs from Slack Audit Logs API as provenance artifacts | Required for compliance use cases |
team:read |
Retrieve team metadata including team_id for org-token calls |
Required for Enterprise Grid / org tokens |
Token type guidance: A bot token (xoxb-) only sees channels where the bot is a member. For a complete workspace inventory, use a User Token (xoxp-) issued by a Workspace Owner. On Enterprise Grid with an org-level token, include the team_id parameter on every API call or you will receive results scoped only to the primary workspace.
Box Application Configuration
Box supports two primary app types for programmatic access. The choice has compliance implications.
| Factor | Box JWT App (Service Account) | Box OAuth 2.0 App (User-Delegated) |
|---|---|---|
| Authentication | RSA key pair, no user login required | User grants access via OAuth flow |
| Audit trail attribution | Actions attributed to service account | Actions attributed to the delegating user |
| Rate limit budget | Shared across service account | Per-user budget |
| Legal defensibility | Weaker chain of custody (single actor) | Stronger — preserves user identity in audit logs |
| Best for | Automated pipelines, workspace shutdown scenarios | Compliance-driven archives where user attribution matters |
Required Box API scopes (configured in the Box Developer Console):
| Scope | Purpose |
|---|---|
root_readwrite |
Create folders and upload files |
manage_managed_users |
Look up Box users by email for collaboration mapping |
manage_groups |
Optional: map Slack channels to Box groups |
manage_retention_policies |
Apply retention policies to uploaded files |
manage_legal_holds |
Apply legal holds to files or folders |
metadata_templates_write |
Create metadata templates (admin only) |
metadata_instances_write |
Write metadata instances to files |
How to Inventory Canvases When There Is No canvases.list
To programmatically list all canvases in a workspace, use files.list with types=canvas: https://slack.com/api/files.list?types=canvas. There is no dedicated canvases.list endpoint, which is a major constraint when exporting data from Slack Canvas. Canvases are technically file objects in Slack's backend, mixed in with PDFs, images, and code snippets, so filtering by types=canvas is the only reliable way to isolate them.
The response includes each canvas's id, name, created timestamp, user (creator), and the channels array showing where it has been shared. Standalone canvases — created independently and shared selectively — appear without a channel association. Track these separately.
files.list is rate-limited at Tier 3: 50+ requests per minute. Each response returns a page of results. For workspaces with thousands of canvases, expect to paginate heavily using cursor-based pagination via the next_cursor value in response_metadata. Filter by channel to scope the inventory per-channel, which also gives you the channel-to-canvas mapping you need later for folder construction.
import time
import requests
def list_canvases(token, channel=None):
canvases = []
cursor = None
while True:
params = {"types": "canvas", "count": 100}
if channel:
params["channel"] = channel
if cursor:
params["cursor"] = cursor
resp = requests.get(
"https://slack.com/api/files.list",
headers={"Authorization": f"Bearer {token}"},
params=params,
).json()
canvases.extend(resp.get("files", []))
cursor = resp.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
time.sleep(1.2) # respect Tier 3 rate limit: 1 req/1.2s ≈ 50/min
return canvasesBot tokens only see channels the bot has joined. A bot token (xoxb-) will only return canvases from channels where the bot is a member. For a complete workspace inventory, either add the bot to every public and private channel programmatically, or use a User Token (xoxp-) from a Workspace Owner with files:read scope. If you are using an org-level token, Slack requires the team_id parameter on every call.
Three Canvas Contexts
Canvases exist in three contexts that must be handled separately throughout the pipeline:
| Context | How Identified | Folder Destination | Permission Source |
|---|---|---|---|
| Channel canvas | channels array is non-empty |
#{channel-name}/ folder |
conversations.members |
| DM canvas | ims or mpim array is non-empty, channels is empty |
_Direct Messages/{participants}/ |
DM participant list |
| Standalone canvas | All arrays empty | _Standalone/ |
canvases.access.set grants |
Capture id, title, edit_timestamp, last_editor, channels, groups, and ims in your inventory manifest. You need this provenance data for folder construction, metadata tagging, and permission mapping.
Deduplication: Canvases Shared to Multiple Channels
A canvas shared into multiple channels appears in the files.list response for each channel — the same canvas_id with multiple entries in the channels array. You have two valid approaches:
Option A: Canonical copy with manifest references. Store the canvas file once in the channel where it was originally created (determined by the created timestamp and user field). Record all secondary channel associations in the manifest JSON and as metadata on the Box file. Cleaner audit trail; avoids storage duplication.
Option B: Duplicate files per channel, each with a dedup flag. Store a copy in each channel folder and add a is_canonical_copy: true/false field in the metadata template. Simpler folder browsing; compliance teams can query by channel without cross-referencing the manifest.
Recommendation for compliance use cases: Option A with manifest references. Duplication creates ambiguity about which copy controls under a legal hold. A single canonical file with explicit provenance metadata is more defensible.
Canvas Version History Limitation
Slack does not expose full canvas edit history via API. canvases.getContent returns the current state only — there is no endpoint equivalent to Google Docs' revision history or SharePoint's version list. What partial history is available:
- The
edit_timestampandlast_editorfields on the file object record the most recent edit only - The Slack Audit Logs API (
/audit/v1/logs) records canvas edit events with actor, timestamp, and canvas ID — but not the content state at each edit
For compliance use cases where version history matters, export the Audit Logs API records for each canvas ID as a companion JSON file alongside the archived canvas. This gives you a timestamped edit trail (who edited, when) even though the content states are unrecoverable.
What the Exported Markdown Actually Contains
Use canvases.getContent to export each canvas body. Slack returns the full canvas as either markdown or HTML, defaulting to markdown. The entire body comes back as a single content string with no pagination. (docs.slack.dev)
curl -X POST https://slack.com/api/canvases.getContent \
-H "Authorization: Bearer $SLACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"canvas_id":"F12345678","content_type":"markdown"}'The markdown subset Slack supports is narrower than GitHub-Flavored Markdown:
- Headings cap at H3. Canvas supports H1, H2, and H3 only. If your destination system relies on deeper nesting for table of contents generation, you will need to infer structure programmatically.
- Tables cap at 300 cells. No formulas, no merged cells, no column types. If users treated a Canvas table like a lightweight spreadsheet, only the static values are exported.
- Supported elements include bold, italic, strikethrough, bulleted and ordered lists, checklists, code blocks, quotes, dividers, links, tables, and @mentions.
- Markdown content is limited to 1 MiB (1,048,576 characters) per
document_contentobject. - Nested formatting in certain combinations can cause
canvas_editing_failederrors — for example, a list inside a blockquote. - Interactive elements degrade. Buttons, workflow triggers, and live embeds (Jira tickets, unfurls, etc.) degrade into plain-text URLs or are stripped entirely from the markdown payload.
Slack-specific syntax also appears in the export: user mentions as <@U12345>, channel references as <#C12345>, and emoji shortcodes like :white_check_mark:. None of these render in Box. Your transform step must resolve user mentions to display names (using users.info), convert channel references to readable names (using conversations.info), and strip or convert emoji codes before writing files to Box.
Detecting Canvases That Approach the 1 MiB Limit
The 1 MiB limit applies to the document_content object within the API. In practice, most canvases are well under this threshold — a typical formatted runbook or meeting notes document is 10–50 KB. Canvases that risk hitting the limit are those used as long-running knowledge bases with extensive embedded content. To detect these before the upload phase:
def flag_large_canvases(canvas_body: str, threshold_bytes: int = 900_000) -> bool:
"""Flag canvases within 10% of the 1 MiB API limit."""
return len(canvas_body.encode("utf-8")) >= threshold_bytesFlag any canvas exceeding 900 KB for manual review before running the upload phase. These may need to be split, summarized, or handled as PDF-only archives.
Markdown vs. PDF: Choosing the Landing Format
Neither format preserves both structure and appearance. This is the central trade-off of the entire operation.
| Factor | Markdown (.md) | |
|---|---|---|
| Structure preservation | High — headings, lists, tables survive as semantic elements | Low — flattened to visual layout |
| Visual fidelity | Low — no styling, no branded look | High — renders as the canvas appeared |
| Searchability in Box | Full-text indexed by Box | Full-text indexed (Box extracts PDF text) |
| Editability after landing | Editable in any text editor | Read-only |
| File size | Small (KB range for most canvases) | Larger (especially with embedded images) |
| eDiscovery compatibility | Requires rendering for review tools | Natively supported by Relativity, Everlaw, Exterro |
Our recommendation: Store both. Upload the .md file as the primary governed record — searchable, diffable, lightweight — and attach a PDF rendition as a sibling file for stakeholders who need visual fidelity or eDiscovery review tool compatibility. A paired landing of Canvas Name.md and Canvas Name.snapshot.pdf, alongside a small manifest.json, gives you structure and appearance in two files.
Generate PDF from Slack HTML, not from already-flattened markdown. The canvases.getContent API returns both HTML and markdown. Use markdown for the searchable master and HTML-to-PDF (via Puppeteer or a similar renderer) for the visual snapshot. This preserves more of the original canvas appearance than rendering markdown to PDF would — Slack's HTML output retains heading styles and table formatting that the markdown-to-PDF conversion path flattens further.
How to Build a Folder Tree When Canvas Has No Folders
Slack Canvas has no folder concept. Canvases live inside channels or float as standalone documents. Box is organized entirely around folders. You have to invent the hierarchy.
The most practical approach:
- One Box folder per Slack channel. Name it
#{channel-name}or use the channel's topic/purpose as a description on the Box folder. - Standalone canvases go into a top-level
_Standalonefolder. - Index canvases — every Slack channel has an optional canvas pinned in the channel header as a table of contents. Treat these index canvases as README files in the channel folder. Name them
[Channel_Name]_Index_Canvasto prevent naming collisions. - DM canvases go into a
_Direct Messagesfolder, with subfolders named by participant pair or group.
Slack Canvas Archive/
├── #engineering/
│ ├── onboarding-runbook.md
│ ├── onboarding-runbook.snapshot.pdf
│ ├── incident-response-template.md
│ ├── incident-response-template.snapshot.pdf
│ └── engineering_Index_Canvas.md
├── #legal-ops/
│ ├── contract-review-process.md
│ └── retention-policy-notes.md
├── _Standalone/
│ ├── quarterly-okrs.md
│ └── board-deck-draft.md
├── _Direct Messages/
│ └── alice-bob/
│ └── project-handoff.md
└── manifest.json
Create all folders in Box before uploading files. The Box API's folder creation endpoint requires a parent folder ID for every upload — batch your folder creation first and cache the IDs in a local database or JSON file keyed by Slack channel ID.
Use channel IDs and canvas IDs in Box metadata even if the human-facing folder names use readable channel titles. Names change. IDs do not. If teams used index canvases as manual tables of contents, parse them and store their link order in the manifest so the Box archive reflects human navigation, not just API enumeration order.
Mapping Canvas Permissions to Box Collaborations
Slack Canvas permissions and Box collaborations operate on fundamentally different models. Slack permissions are dynamic and largely inherited from channel membership. Box permissions are explicit and hierarchical, defined by Collaboration objects.
The canvases.access.set method sets the access level to a canvas for specified entities. Both channel_ids and user_ids cannot be passed at the same time. Access levels are read (grants read access), write (grants read and write access), and owner (makes the specified user the owner — only users can be owners, not channels). For channel canvases, access is tied to channel membership by default — there are no separate access implications unless explicit overrides have been set via canvases.access.set.
Here is the practical mapping:
| Canvas Access Level | Box Collaboration Role | Notes |
|---|---|---|
| Channel membership (implicit read) | Viewer on channel folder | All channel members get Viewer |
read (explicit via canvases.access.set) |
Viewer on file | Applied per-file for standalone canvases |
write |
Editor on file | Or Editor on folder if all canvases in channel were writable |
owner |
Co-owner | Avoids overstating control — safer and easier to audit than mapping to full Owner |
Translating Channel Membership
For channel-scoped canvases:
- Query
conversations.membersfor the channel (conversations.membersis Tier 4: 100+ requests per minute). - Call
users.infoon each member to retrieve their email address. - Look up the corresponding Box user account by email using
GET /users?filter_term={email}. - Create a Box Collaboration on the channel folder, granting matched users
ViewerorEditorroles based on the mapping table above.
Your main ACL boundary should be the folder that represents the Slack channel, not hundreds of per-file permissions. Only apply file-level collaborations when a standalone canvas has explicit access grants that differ from the folder's scope.
The Timestamp Limitation
Box collaboration timestamps cannot be preserved. The created_at field on a Box collaboration records when the collaboration object was created — it is system-set at write time. You cannot backdate it to match when a user was originally granted access in Slack. If your compliance process requires original grant timestamps, store them as metadata on the file, in the manifest.json, or export them from Slack's Audit Logs API as a companion provenance artifact.
Rate Limits That Shape the Timeline
Two API boundaries control how fast this operation runs.
Slack side:
files.list— Tier 3: 50+ requests per minutecanvases.getContent— Tier 3: 50+ requests per minuteconversations.members— Tier 4: 100+ requests per minuteusers.info— Tier 4: 100+ requests per minute- Expect roughly 50 canvas body reads per minute if you serialize requests with a 1.2-second sleep
Box side:
- General API calls: 1,000 requests per minute per user
- File uploads: 240 file upload requests per minute per user
- No bulk upload endpoint — every file is a separate
POST /files/contentcall 429 Too Many Requestsresponses include aRetry-Afterheader specifying the backoff duration; ignoring this header results in extended temporary bans
Rough math for 2,000 canvases:
| Step | Rate Limit | Estimated Time |
|---|---|---|
| Inventory (files.list pagination) | ~50 req/min | ~5 min |
| Read canvas bodies | ~50/min (Slack Tier 3) | ~40 min |
| Resolve user mentions + channel refs | ~100/min (Tier 4) | ~20 min |
| Upload .md files to Box | 240 uploads/min | ~9 min |
| Upload .pdf files to Box | 240 uploads/min | ~9 min |
| Create collaborations | 1,000 req/min | Varies by permission count |
| Apply metadata instances | 1,000 req/min | ~2–4 min |
| Total wall-clock time | ~90–120 min |
Parallelizing across multiple Box service accounts — each with its own 1,000 req/min budget — is the standard way to reduce wall-clock time for larger workspaces. On the Slack side, user token rate limits are per-user, so a single token is your bottleneck for the read phase.
Your script must implement exponential backoff on all requests. When Box or Slack returns a 429, read the Retry-After header and pause execution for at least that duration before retrying. Do not assume Box's bulk CLI helpers (--bulk-file-path) bypass rate limits — those are wrappers over ordinary item-by-item operations.
Idempotency and Failure Recovery
Any pipeline operating at this scale will fail mid-run — a token expires, a rate limit ban hits, a network timeout occurs. Without idempotency controls, rerunning the pipeline re-uploads files, duplicates metadata, and creates duplicate collaborations. Build idempotency in from the start.
State Tracking with a Local Manifest
Maintain a manifest.json (or SQLite database for large workspaces) that records the pipeline state for each canvas:
{
"F12345678": {
"canvas_id": "F12345678",
"title": "Incident Response Runbook",
"channel": "C87654321",
"box_file_id_md": "987654321",
"box_file_id_pdf": "987654322",
"box_folder_id": "111222333",
"metadata_applied": true,
"collaborations_applied": true,
"links_rewritten": false,
"audit_log_exported": true,
"status": "partial"
}
}Before processing any canvas, check the manifest. Skip steps whose completion flag is true. This makes every pipeline run a resume operation rather than a restart.
Handling Box Conflict Errors on Re-upload
When uploading a file that already exists in Box, the API returns 409 Conflict with an error body containing the existing file's ID. Use this to recover gracefully:
def upload_or_get_existing(folder_id, filename, content, token):
response = box_upload(folder_id, filename, content, token)
if response.status_code == 409:
# File exists — extract the existing file ID from the conflict error
existing_id = response.json()["context_info"]["conflicts"]["id"]
return existing_id
response.raise_for_status()
return response.json()["entries"][0]["id"]Record the returned file ID in the manifest regardless of whether the file was newly uploaded or already existed. Subsequent steps (metadata, collaborations, link rewriting) operate on the file ID, not the upload response.
Token Expiry During Long Runs
For long-running pipelines (90+ minutes), Slack user tokens do not expire, but Box OAuth 2.0 access tokens expire after 60 minutes. If using Box OAuth, implement token refresh before each upload batch:
def refresh_box_token_if_needed(token_data):
if time.time() >= token_data["expires_at"] - 60:
# Refresh 60 seconds before expiry
return refresh_box_oauth_token(token_data["refresh_token"])
return token_dataBox JWT tokens have configurable expiry (typically 60 minutes) but can be re-issued on demand. For pipelines longer than 60 minutes, issue a new JWT assertion before each major phase rather than reusing the initial token.
Metadata Templates Must Exist Before Instances Are Written
A Box administrator defines the structure of metadata by creating a metadata template, then associates it with content to create a metadata instance. Creating metadata templates is restricted to users with admin permission. The template must exist in your Box enterprise before you write any metadata instances — the API will reject instance creation calls that reference a nonexistent template.
If a template requires a specific enum value (e.g., archive_format: markdown), and your script attempts to write a value not pre-approved in the template schema, the API rejects the entire metadata payload with a 400 Bad Request.
A practical template for archived canvases:
{
"scope": "enterprise",
"displayName": "Slack Canvas Archive",
"templateKey": "slackCanvasArchive",
"fields": [
{"type": "string", "key": "slack_canvas_id", "displayName": "Canvas ID"},
{"type": "string", "key": "slack_channel_id", "displayName": "Source Channel ID"},
{"type": "string", "key": "slack_channel_name", "displayName": "Source Channel Name"},
{"type": "string", "key": "original_creator_email", "displayName": "Original Creator Email"},
{"type": "date", "key": "canvas_created_at", "displayName": "Canvas Created At"},
{"type": "date", "key": "canvas_last_edited", "displayName": "Last Edited"},
{"type": "string", "key": "last_editor_email", "displayName": "Last Editor Email"},
{"type": "enum", "key": "archive_format", "displayName": "Format", "options": [{"key": "markdown"}, {"key": "pdf"}, {"key": "manifest"}]},
{"type": "enum", "key": "canvas_context", "displayName": "Canvas Context", "options": [{"key": "channel"}, {"key": "dm"}, {"key": "standalone"}]},
{"type": "string", "key": "canonical_canvas_id", "displayName": "Canonical Canvas ID (dedup)"},
{"type": "enum", "key": "is_canonical_copy", "displayName": "Is Canonical Copy", "options": [{"key": "true"}, {"key": "false"}]}
]
}This template preserves provenance data that Box's file-level created_at and modified_at timestamps cannot capture, since Box sets those at upload time rather than reflecting the original Slack dates. The canonical_canvas_id and is_canonical_copy fields support the deduplication approach described earlier.
Slack Audit Logs as Provenance Artifacts
For compliance use cases, export canvas-related events from the Slack Audit Logs API (/audit/v1/logs) as companion artifacts alongside each archived canvas. This API requires Slack Enterprise Grid and the audit:read scope.
Query for canvas-specific events:
curl "https://api.slack.com/audit/v1/logs?action=canvas_created&oldest=1672531200" \
-H "Authorization: Bearer $SLACK_AUDIT_TOKEN"Relevant action types for canvas archival provenance:
| Audit Event | Meaning |
|---|---|
canvas_created |
Canvas was created; records creator and channel |
canvas_updated |
Canvas content was edited; records editor and timestamp |
canvas_access_updated |
Access level was changed via canvases.access.set |
canvas_deleted |
Canvas was deleted (useful for verifying completeness of inventory) |
Store the filtered audit log for each canvas as {canvas_id}_audit_log.json in the same Box folder as the canvas file. Apply the same metadata template and retention policy. This gives compliance teams a timestamped edit trail even though content states at each edit are unrecoverable from the API.
Box Shield and Classification Label Considerations
If your Box enterprise uses Box Shield for automatic content classification, archived canvases may trigger classification rules on upload. This is generally desirable for compliance use cases — but be aware of two edge cases:
Classification labels may trigger DLP policies. If Shield is configured to block external sharing of content classified above a certain sensitivity level, and your pipeline creates collaborations immediately after upload, those collaboration creation calls may fail with a 403 Forbidden if Shield intercepts them. Upload files first, apply metadata, then create collaborations — and handle Shield-blocked collaboration attempts with a specific error log for manual review.
Existing classification templates may conflict with the Slack Canvas Archive template. Box metadata templates are scoped per enterprise and can conflict if field keys overlap. Before creating the slackCanvasArchive template, verify that no existing enterprise template uses the same templateKey or field keys.
Edge Cases That Will Break Your First Run
Slack-specific mentions. Canvas markdown contains <@U12345678> for user mentions and <#C12345678> for channel references. Resolve these to display names before upload using users.info and conversations.info respectively. Unresolved angle-bracket strings render as literal text in Box.
Embedded images. Canvases can reference images hosted on Slack's CDN (files.slack.com). These URLs require Slack authentication and will break once the workspace is deleted or the token expires. Download every referenced image during the read phase, upload it to Box as a separate file in the same folder, and rewrite the markdown link to point to the new Box file URL. Track image downloads in the manifest to avoid re-downloading on reruns.
Tables exceeding 300 cells. The API enforces the 300-cell limit. If a user built a large table by pasting from a spreadsheet, the export may truncate or return an error. Inspect table sizes during the read phase by counting pipe characters in the markdown output and flag tables over ~280 cells for manual review.
Checklist state. Canvas checklists have checked/unchecked state. The markdown export represents this as - [x] and - [ ] syntax. This survives in .md files but becomes purely visual in PDF. Box has no native checklist feature — the state is preserved as text only.
Canvas-to-canvas links. Canvases can link to other canvases using internal Slack URLs (https://app.slack.com/docs/{team_id}/{canvas_id}). These links break in Box. Build a link-rewriting pass as the final step after all uploads complete: scan all .md files for Slack canvas URLs, look up the target canvas ID in the manifest, and replace with the corresponding Box file URL.
Private channel visibility gaps. The Slack file object only returns private-group IDs when the caller is a member of that group. Verify private channel coverage by cross-referencing groups.list against the channels represented in your inventory. Any channel in groups.list that has zero canvases in the inventory despite being active is likely a visibility gap, not an empty channel.
Nested formatting errors. Certain nested markdown combinations (list inside blockquote, checklist inside table cell) may return canvas_editing_failed errors from canvases.getContent. Log these with the canvas ID and fall back to the HTML export for those canvases. HTML is more permissive and will typically succeed where markdown export fails.
API Error Reference
| Error | Source | Cause | Correct Handling |
|---|---|---|---|
canvas_editing_failed |
Slack | Unsupported nested markdown combination | Retry with content_type: html; log canvas ID |
429 Too Many Requests |
Slack or Box | Rate limit exceeded | Read Retry-After header; sleep exact duration; retry |
409 Conflict |
Box | File already exists in folder | Extract existing file ID from context_info.conflicts.id; skip upload; continue |
400 Bad Request on metadata |
Box | Enum value not in template schema, or template does not exist | Verify template exists and enum options match; do not retry without fixing the payload |
403 Forbidden on collaboration |
Box | Box Shield DLP policy blocked the action | Log for manual review; do not retry automatically |
404 Not Found on canvas |
Slack | Canvas was deleted between inventory and export | Mark as deleted_before_export in manifest; skip |
invalid_auth |
Slack | Token expired or revoked | Halt pipeline; re-authenticate; resume from manifest |
ratelimited |
Slack | Same as 429 but returned as JSON body error | Read retry_after field in response body; sleep and retry |
How Box Retention Policies Apply to Archived Canvases
Box supports retention policies with a disposition action at the global, folder, or file (via metadata) level across content. Once canvases land in Box as files, they inherit whatever retention policy applies to their folder — or you can apply file-level retention via metadata-driven policies.
Legal holds in Box preserve content for litigation by placing users or folders on hold for a set period of time, or continuously until the legal matter ends. This is the primary reason compliance teams move canvases to Box: Slack's legal holds are only available on Enterprise Grid, and even there, canvas version history is limited and canvases are not included in standard Slack exports.
Box Governance integrates with leading eDiscovery tools including Relativity, Exterro, and Everlaw to proactively preserve, analyze, collect, and review data, reducing the risk of data spoliation. Archived canvases in Box become first-class objects in these review workflows; canvases left in Slack do not.
Step-by-Step Archival Sequence
-
Configure credentials. Set up Slack User Token with all required scopes (table above). Configure Box app (JWT or OAuth) with required scopes. Verify private channel access by cross-referencing
groups.listagainst expected channels. -
Create Box metadata template. Requires Box admin permissions. Use the template schema above. Verify the template exists and all enum values are correct before proceeding. Do this before any file uploads — instance creation calls will fail without the template.
-
Initialize manifest. Create
manifest.jsonwith an entry for every canvas to be archived, status set topending. This file drives idempotency for all subsequent steps. -
Inventory all canvases using
files.list?types=canvas, iterating per-channel and capturing standalone canvases separately. Record canvas ID, name, creator, timestamps, and channel associations. Resolve deduplication — flag canvases shared to multiple channels and designate canonical copies. -
Export Slack Audit Logs for canvas events (Enterprise Grid only). Store per-canvas audit records as
{canvas_id}_audit_log.jsonfor use as provenance artifacts. -
Build the Box folder tree — one folder per channel, plus
_Standaloneand_Direct Messagescontainers. Cache all folder IDs in the manifest. Do not upload files until all folders exist. -
Read each canvas body using
canvases.getContent. Export markdown (primary) and HTML (for PDF generation). Resolve<@U...>mentions and<#C...>references. Download embedded CDN images. Flag canvases over 900 KB or with tables over 280 cells for manual review. Update manifest status toexported. -
Transform content. Produce
.mdfile. Generate.pdffrom HTML via Puppeteer or equivalent renderer. Update manifest status totransformed. -
Upload files to Box into the appropriate folder. Use the
upload_or_get_existingpattern to handle conflicts idempotently. Record Box file IDs in the manifest. Respect 240-upload-per-minute limit. Refresh Box tokens if the pipeline will exceed 60 minutes. Update manifest status touploaded. -
Apply metadata instances to each file using the template from step 2. Include: Slack canvas ID, source channel ID and name, original creator email, Slack timestamps, archive format, canvas context, and deduplication fields. Update manifest
metadata_appliedtotrue. -
Create Box collaborations on folders and files, mapping from channel membership and
canvases.access.setgrants. Set folder-level ACLs first, then file-level exceptions only where needed. Handle Shield-blocked403responses by logging for manual review. Update manifestcollaborations_appliedtotrue. -
Apply retention policies and legal holds to folders or files as required by your compliance configuration. This step requires Box Governance entitlements.
-
Rewrite internal links. Scan all uploaded
.mdfiles for Slack canvas URLs. Look up target canvas IDs in the manifest and replace with Box file URLs. Update manifestlinks_rewrittentotrue. -
Upload audit log files to the appropriate Box folder. Apply metadata and retention policies consistent with the canvas files.
-
Validate. Compare canvas inventory count against Box file count. (For a deeper dive into reconciliation, see our Slack Canvas validation guide.) Verify manifest completeness — every entry should have all completion flags set to
true. Spot-check 5–10% of files for unresolved mentions, broken image links, and metadata accuracy. Cross-reference collaboration counts against channel membership counts for a sample of channels.
What You Lose in This Archival
Be explicit with stakeholders about what the Box destination cannot replicate:
- In-place editing. Box can preview
.mdfiles and render PDFs, but there is no equivalent to Canvas's inline, multi-user editing within a Slack channel. - Slack-native context. A canvas pinned in
#engineeringsits next to the conversation it supports. In Box, it is a file in a folder — the conversational context is absent. - Reactions and comments. Canvas comments and emoji reactions do not transfer. You can preserve them as appended text in the
.mdfile, but the interactive layer is gone. - Checklist interactivity. Checkboxes become static text in both Markdown and PDF.
- Real-time co-authoring. Box supports collaborative editing on Box Notes and Office files, but not on plain
.mdfiles. - Full version history. Only the current canvas state is recoverable via API. Audit log events record who edited and when, but not the content at each prior state.
None of this is a reason not to archive — it is a reason to set expectations correctly. The goal is governed records, not a replacement collaboration surface.
For related technical guides, see our Box to SharePoint migration guide for the reverse direction, our Box to Slack Canvas migration guide if you are moving files into Slack, or the Slack Enterprise Grid migration guide if canvases are part of a broader Slack consolidation.
Frequently Asked Questions
- Can you migrate Slack Canvas to Box without losing editing?
- No. Slack exports canvas content as markdown or HTML, but moving it into Box turns a live collaboration surface into static files. In-place editing, co-authoring, and checklist interactivity are permanently lost. What you gain is Box's governance layer: retention policies, legal holds, classifications, and eDiscovery integration.
- How do you list all canvases in a Slack workspace via API?
- There is no canvases.list endpoint. Use the files.list method with the types=canvas filter. This is rate-limited at Tier 3 (50+ requests per minute). Bot tokens only return canvases from channels the bot has joined — use a Workspace Owner user token for a complete inventory.
- What format does Slack Canvas export to?
- The canvases.getContent API exports content as markdown or HTML. The markdown subset supports headings up to H3, tables capped at 300 cells with no formulas, lists, code blocks, and links. Content is limited to 1 MiB per document. There is no native PDF export — PDF requires a downstream rendering step.
- What are the Box API rate limits for file uploads?
- Box allows 1,000 general API requests per minute per user and 240 file upload requests per minute per user. There is no bulk upload endpoint — each file requires a separate API call. Exceeding limits triggers HTTP 429 throttling with a Retry-After header.
- Can Box collaborations preserve original Slack permission timestamps?
- No. Box collaboration timestamps are system-generated at the moment the collaboration is created via the API. You cannot backdate them. Store original Slack grant timestamps as metadata on the file or in a separate manifest.