This document provides detailed information about the Slack REST APIs used for uploading files to Slack channels.
The original files.upload API method was deprecated on May 16, 2024 and will be completely sunset on November 12, 2025.
Why it was deprecated:
- Synchronous upload process didn't scale well with large files
- Single HTTP request had to wait for entire upload and processing
- Not optimized for modern file sizes and upload patterns
The new approach uses three separate API calls for better performance and scalability:
- files.getUploadURLExternal - Request an upload URL
- HTTP POST - Upload file content to the URL
- files.completeUploadExternal - Finalize and optionally share
Requests an upload URL and file ID from Slack.
Endpoint: https://slack.com/api/files.getUploadURLExternal
Method: POST
Authentication: Bearer token in Authorization header
Required Scopes: files:write
| Parameter | Type | Required | Description |
|---|---|---|---|
filename |
string | Yes | Name of the file being uploaded |
length |
integer | Yes | Size of the file in bytes |
curl --location 'https://slack.com/api/files.getUploadURLExternal' \
--header 'Authorization: Bearer xoxb-your-token-here' \
--form 'filename="resume.pdf"' \
--form 'length="245678"'import requests
url = "https://slack.com/api/files.getUploadURLExternal"
headers = {"Authorization": "Bearer xoxb-your-token-here"}
data = {
"filename": "resume.pdf",
"length": "245678"
}
response = requests.post(url, headers=headers, data=data)
result = response.json(){
"ok": true,
"upload_url": "https://files.slack.com/upload/v1/abcdefghijklmnop",
"file_id": "F012AB3CDE4"
}| Field | Type | Description |
|---|---|---|
ok |
boolean | Indicates if the request was successful |
upload_url |
string | URL to upload the file content to (Step 2) |
file_id |
string | Unique identifier for this file |
{
"ok": false,
"error": "invalid_auth"
}| Error | Description | Solution |
|---|---|---|
invalid_auth |
Invalid authentication token | Check your token is correct and starts with xoxb- |
missing_scope |
Token lacks required scope | Add files:write scope to your app |
invalid_arguments |
Missing or invalid parameters | Ensure filename and length are provided |
Upload the actual file content to the URL received from Step 1.
Endpoint: The upload_url from Step 1 (e.g., https://files.slack.com/upload/v1/...)
Method: POST
Authentication: None required (URL is pre-authenticated)
Content-Type: application/octet-stream
curl --location 'https://files.slack.com/upload/v1/abcdefghijklmnop' \
--header 'Content-Type: application/octet-stream' \
--data-binary '@resume.pdf'import requests
upload_url = "https://files.slack.com/upload/v1/abcdefghijklmnop"
headers = {"Content-Type": "application/octet-stream"}
with open("resume.pdf", "rb") as file:
file_content = file.read()
response = requests.post(upload_url, headers=headers, data=file_content)HTTP Status: 200 OK
OK - 245678
The response body contains "OK" followed by the number of bytes uploaded.
| HTTP Status | Description |
|---|---|
| 400 | Bad Request - Invalid file data |
| 403 | Forbidden - Upload URL expired or invalid |
| 413 | Payload Too Large - File exceeds size limit |
Finalizes the upload and optionally shares the file to channels.
Endpoint: https://slack.com/api/files.completeUploadExternal
Method: POST
Authentication: Bearer token in Authorization header
Required Scopes: files:write
| Parameter | Type | Required | Description |
|---|---|---|---|
files |
JSON array | Yes | Array of file objects with id and optional title |
channel_id |
string | No | Channel ID to share the file to |
channels |
string | No | Comma-separated list of channel IDs |
initial_comment |
string | No | Message text to accompany the file |
thread_ts |
string | No | Thread timestamp to share file in a thread |
[
{
"id": "F012AB3CDE4",
"title": "My Resume"
}
]curl --location 'https://slack.com/api/files.completeUploadExternal' \
--header 'Authorization: Bearer xoxb-your-token-here' \
--form 'files="[{\"id\":\"F012AB3CDE4\",\"title\":\"My Resume\"}]"' \
--form 'channel_id="C0123456789"' \
--form 'initial_comment="Here is my resume for review"'import requests
import json
url = "https://slack.com/api/files.completeUploadExternal"
headers = {"Authorization": "Bearer xoxb-your-token-here"}
files_data = [{"id": "F012AB3CDE4", "title": "My Resume"}]
data = {
"files": json.dumps(files_data),
"channel_id": "C0123456789",
"initial_comment": "Here is my resume for review"
}
response = requests.post(url, headers=headers, data=data)
result = response.json()# Omit channel_id to upload without sharing
data = {
"files": json.dumps([{"id": "F012AB3CDE4", "title": "Private Document"}])
}{
"ok": true,
"files": [
{
"id": "F012AB3CDE4",
"created": 1699564800,
"timestamp": 1699564800,
"name": "resume.pdf",
"title": "My Resume",
"mimetype": "application/pdf",
"filetype": "pdf",
"pretty_type": "PDF",
"user": "U012A3456BC",
"size": 245678,
"mode": "hosted",
"is_external": false,
"is_public": true,
"url_private": "https://files.slack.com/files-pri/T012-F012/resume.pdf",
"url_private_download": "https://files.slack.com/files-pri/T012-F012/download/resume.pdf",
"permalink": "https://yourworkspace.slack.com/files/U012/F012/resume.pdf",
"channels": ["C0123456789"],
"shares": {
"public": {
"C0123456789": [
{
"ts": "1699564800.123456"
}
]
}
}
}
]
}| Field | Type | Description |
|---|---|---|
ok |
boolean | Indicates if the request was successful |
files |
array | Array of file objects with detailed information |
files[].id |
string | File ID |
files[].name |
string | Original filename |
files[].title |
string | Display title |
files[].size |
integer | File size in bytes |
files[].mimetype |
string | MIME type of the file |
files[].permalink |
string | Permanent link to the file |
files[].channels |
array | List of channel IDs where file is shared |
| Error | Description | Solution |
|---|---|---|
invalid_auth |
Invalid authentication token | Verify your token |
file_not_found |
File ID doesn't exist | Check the file_id from Step 1 |
channel_not_found |
Channel ID is invalid | Verify channel ID and bot membership |
not_in_channel |
Bot not in the specified channel | Invite bot to channel first |
import os
import json
import requests
class SlackFileUploader:
BASE_URL = "https://slack.com/api"
def __init__(self, token):
self.token = token
self.headers = {"Authorization": f"Bearer {token}"}
def upload_file(self, file_path, channel_id, title=None, comment=None):
# Get file info
filename = os.path.basename(file_path)
file_size = os.path.getsize(file_path)
# Step 1: Get upload URL
response = requests.post(
f"{self.BASE_URL}/files.getUploadURLExternal",
headers=self.headers,
data={"filename": filename, "length": file_size}
)
result = response.json()
upload_url = result["upload_url"]
file_id = result["file_id"]
# Step 2: Upload file content
with open(file_path, "rb") as f:
requests.post(
upload_url,
headers={"Content-Type": "application/octet-stream"},
data=f.read()
)
# Step 3: Complete upload
files_data = [{"id": file_id, "title": title or filename}]
data = {"files": json.dumps(files_data)}
if channel_id:
data["channel_id"] = channel_id
if comment:
data["initial_comment"] = comment
response = requests.post(
f"{self.BASE_URL}/files.completeUploadExternal",
headers=self.headers,
data=data
)
return response.json()
# Usage
uploader = SlackFileUploader("xoxb-your-token")
result = uploader.upload_file(
file_path="resume.pdf",
channel_id="C0123456789",
title="My Resume",
comment="Please review my resume"
)Slack API uses rate limiting to ensure fair usage:
- Tier 3 methods (including file upload methods): 20+ requests per minute
- Rate limits are per workspace and per token
- Exceeding limits returns HTTP 429 with
Retry-Afterheader
- Implement exponential backoff for retries
- Cache upload URLs if uploading multiple files
- Batch operations when possible
- Monitor rate limit headers in responses
| Plan Type | Maximum File Size |
|---|---|
| Free | 1 GB |
| Pro | 5 GB |
| Business+ | 20 GB |
| Enterprise Grid | 20 GB |
- Use Bot Tokens (
xoxb-) instead of user tokens - Never commit tokens to version control
- Store in environment variables or secure vaults
- Rotate tokens regularly
- Use minimum required scopes
- Files are scanned for malware before being made available
- Larger files may take longer to scan
- Private files (not shared to channels) are only accessible via direct link
- Public files are accessible to all workspace members
Symptoms: HTTP 403 or 400 when uploading to the URL
Causes:
- Upload URL expired (valid for limited time)
- File size doesn't match the
lengthparameter from Step 1 - Network interruption during upload
Solutions:
- Restart the process from Step 1
- Verify file size is correct
- Implement retry logic with fresh URLs
Symptoms: Upload succeeds but file not visible in channel
Causes:
channel_idnot provided in Step 3- Bot not a member of the channel
- Channel ID is incorrect
Solutions:
- Verify you're passing
channel_idparameter - Invite bot to channel:
/invite @YourBot - Double-check channel ID format (starts with
C)
Symptoms: API returns missing_scope error
Solutions:
- Go to api.slack.com/apps
- Select your app
- Navigate to "OAuth & Permissions"
- Add
files:writescope under "Bot Token Scopes" - Reinstall app to workspace
- Use the new token
- Official Slack API Documentation
- Working with Files Guide
- File Object Reference
- OAuth Scopes Reference
- Rate Limiting Guide
files.uploadsunset date extended to November 12, 2025
- New apps can no longer use
files.upload files.getUploadURLExternalandfiles.completeUploadExternalbecome standard
- Announcement of
files.uploaddeprecation - Introduction of new upload flow