Slack API upload file: three calls replace files.upload

Slack deprecated files.upload. Call files.getUploadURLExternal, POST the bytes to upload_url, then files.completeUploadExternal with a channel_id.

5 min readSume
All posts

To upload a file with the Slack API, make three calls: files.getUploadURLExternal with the file's filename and its length in bytes, a POST of the raw bytes to the upload_url it returns, then files.completeUploadExternal with the returned file id and a channel_id to share the file in that channel. The old one-call files.upload is deprecated: Slack's reference page says it would stop functioning and be sunset on November 12, 2025.

Slack facts come from its files.upload, files.getUploadURLExternal, files.completeUploadExternal and Working with files pages; Sume facts come from Run webhooks and Runs and results. All were read on 2026-09-28. Sume has no Slack app or connector: your own server receives Sume's webhook for a finished video and makes the Slack calls. If a link is enough, Slack bot to generate video posts the URL instead of uploading.

Is files.upload deprecated?

Yes. According to Slack's changelog, newly created apps have been unable to call files.upload since May 16, 2024, and the two replacement methods are "more reliable, especially when uploading large files." The files.upload reference page lists a method_deprecated error that points to that changelog.

Slack's SDKs wrap the new sequence in one call: uploadV2 in the Node @slack/web-api package, files_upload_v2 in python-slack-sdk, and FilesUploadV2Request in the Java SDK. Without an SDK, the sequence is three HTTP requests.

How do I upload a file to a channel with the API?

Send the two Web API calls with a token in the Authorization header; the middle request goes to the URL Slack hands you.

  • Call files.completeUploadExternal exactly once. Slack discards the upload if you never call it, and you don't need to wait for Slack to finish processing the file first.
  • Both Web API methods accept form-encoded bodies. Slack's own example sends files as a JSON string inside the form, and the bytes as application/octet-stream.
From Slack's files.getUploadURLExternal and files.completeUploadExternal references, read 2026-09-28.
StepSendGet back
1. files.getUploadURLExternalfilename, and length: the file's size in bytesupload_url and file_id
2. POST to upload_urlThe file as raw bytes or a multipart formHTTP 200 on success; any other status is a failure
3. files.completeUploadExternalfiles, an array of file ids with optional titles, plus channel_id and an optional initial_commentThe shared file objects. Without channel_id, the file stays private
// Node 18+. SLACK_BOT_TOKEN needs the files:write scope.
async function slack(method, params) {
  const res = await fetch(`https://slack.com/api/${method}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SLACK_BOT_TOKEN}` },
    body: new URLSearchParams(params), // form-encoded
  });
  const json = await res.json();
  if (!json.ok) throw new Error(`${method}: ${json.error}`);
  return json;
}

async function uploadToChannel(fileUrl, filename, channelId) {
  const src = await fetch(fileUrl); // a public media.sume.com URL
  if (!src.ok) throw new Error(`download answered ${src.status}`);
  const bytes = Buffer.from(await src.arrayBuffer());
  const { upload_url, file_id } = await slack("files.getUploadURLExternal", {
    filename, length: String(bytes.length), // exact size in bytes
  });
  const sent = await fetch(upload_url, { method: "POST", body: bytes,
    headers: { "Content-Type": "application/octet-stream" } });
  if (sent.status !== 200) throw new Error(`upload_url answered ${sent.status}`);
  await slack("files.completeUploadExternal", {
    files: JSON.stringify([{ id: file_id, title: filename }]),
    channel_id: channelId, initial_comment: "Your video is ready",
  });
}

Which scopes and limits apply?

  • Both new methods need the files:write scope on a bot or user token, and Slack rates both as Tier 4: 100+ calls per minute.
  • The bot must be a member of the channel. Otherwise files.completeUploadExternal answers not_in_channel.
  • Slack's help center says you can add files up to 1GB in size. That page is about adding files in Slack itself; the two method references give no general file-size cap. A workspace can also restrict large uploads, which files.getUploadURLExternal reports as file_upload_size_restricted.
  • A length of 0 is refused with missing_argument, so measure the bytes you actually have.

How do I post a video my backend generated?

Upload from the server that receives the finished-video webhook, never from a browser: the Slack token and your Sume API key both stay server-side. For a Sume Format run, the flow looks like this:

  • Sume sends one signed POST to your communication.webhook_url when the run completes or fails. Verify the HMAC-SHA256 signature over the raw body, record the event, and answer 2xx within the 10-second attempt window; Signed webhooks for Sume video runs covers the check. Upload after you answer, because a slow endpoint gets retried.
  • Dedupe on the envelope's request_id, which is the same on every retry, so a retried delivery doesn't post the video twice.
  • Take payload.primary_output_url, the one output to show, or an entry of payload.artifacts[], which carries url, content_type and size_bytes. These media.sume.com URLs are durable and public to anyone holding them, so the download needs no API key.
  • Don't register a Slack URL as Sume's webhook_url. Sume's POST carries its own run receipt, not a Slack message.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume