Uploading Files via Notion’s API

Notion just added the ability to upload files via their public API! This is a feature the developer community has been waiting for since the API launched, and it opens up a whole new world of automation possibilities. No more relying on external file hosting and embedding URLs—now we can programmatically upload files directly to Notion.

What’s New?

The file upload API allows developers to upload files directly to Notion via API requests. This means you can now:

  • Upload small files (<20MB) with a simple two-step process.
  • Handle large files (>20MB) through multi-part uploads.
  • Attach files to properties, page bodies, and page covers.
  • Build more complete automation workflows without manual steps.

How It Works

See the Whimsical document below to get a general idea of how the flow works for uploading files to Notion via the API.

The API follows a straightforward process. I’ll inject some example code from playing around with this new API below. At the end you can find all the code samples in my examples repository. Since there is currently no support for the API in the JavaScript SDK (coming soon), I’m using axios to help make requests to the new endpoints.

1. Create a file upload object

This establishes a pending upload in Notion’s system.

				
					async function createFileUpload(options = { mode: 'single_part' }) {
  try {
    const response = await axios({
      method: 'POST',
      url: 'https://api.notion.com/v1/file_uploads',
      headers: {
        Authorization: `Bearer ${process.env.NOTION_API_TOKEN}`,
        'Notion-Version': '2022-06-28',
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
      data: options,
    });

    return response.data;
  } catch (error) {
    console.error('Error creating file upload:', error);
    throw error;
  }
}

const upload = await createFileUpload();
				
			

At this point we can see the status of the upload is pending and there’s also an upload_url which is where we will be uploading our data to.

				
					{
  "object": "file_upload",
  "id": "1f91c1cc-e3f3-810d-872b-00b2307e7303",
  "created_time": "2025-05-20T19:02:00.000Z",
  "created_by": {
    "id": "b289ce9d-156d-4e21-80b7-07212da8a537",
    "type": "bot"
  },
  "last_edited_time": "2025-05-20T19:02:00.000Z",
  "expiry_time": "2025-05-20T20:02:00.000Z",
  "upload_url": "https://api.notion.com/v1/file_uploads/1f91c1cc-e3f3-810d-872b-00b2307e7303/send",
  "archived": false,
  "status": "pending",
  "filename": null,
  "content_type": null,
  "content_length": null,
  "request_id": "42792711-7c67-4914-b1c8-447d8f629f89"
}
				
			

2. Upload data to the FileUpload object

Start sending the file contents to Notion. For small files, you can upload the file directly in one request. For large files, you must split the content over multiple parts and upload the part with the part number you are uploading (using the part_number parameter).

				
					// Single-part upload
const filePath = path.join(__dirname, 'data/example.png');
const fileStream = fs.createReadStream(filePath);
const file = await createFileUpload();
const upload = await uploadPart(file.id, fileStream);
				
			

Note: you can see what uploadPart does here.

Now we can see the status is uploaded. We also no longer see an upload_url property since the file is uploaded.

				
					{
  "object": "file_upload",
  "id": "1f91c1cc-e3f3-810d-872b-00b2307e7303",
  "created_time": "2025-05-20T19:02:00.000Z",
  "created_by": {
    "id": "b289ce9d-156d-4e21-80b7-07212da8a537",
    "type": "bot"
  },
  "last_edited_time": "2025-05-20T19:02:00.000Z",
  "expiry_time": "2025-05-20T20:02:00.000Z",
  "archived": false,
  "status": "uploaded",
  "filename": "example.png",
  "content_type": "image/png",
  "content_length": 1152915,
  "request_id": "0925374b-c349-4bc4-b541-2554179a5d3e"
}
				
			

3. Complete the file upload (for multi-part only)

Next we “tell” Notion that upload is finished. Once you’ve uploaded all parts, we finish with a final request to the FileUpload object to “complete” the upload. 

Here’s what I’m doing to split the file into multiple parts, upload the parts, and then complete the upload and clean up. Here we use split-file to create parts of a file for upload. We then can clean up these parts afterwards. 

Note: this is where it might get tricky if you’re using an automation platform.

				
					const PART_SIZE = 10 * 1024 * 1024; // 10MB chunks for multi-part upload
const MAX_PARTS = 1000;

const filePath = path.join(__dirname, 'data/example.mp4');
const fileName = path.basename(filePath);
const parts = await splitFile.splitFileBySize(filePath, PART_SIZE);

if (parts.length > MAX_PARTS) {
  throw new Error(`File is too large. Maximum number of parts is ${MAX_PARTS}.`);
}

// Create multi-part upload
const file = await createFileUpload({
  mode: 'multi_part',
  number_of_parts: parts.length,
  filename: fileName,
  content_type: 'video/mp4',
});

let upload;

for (let i = 1; i <= parts.length; i++) {
  const fileStream = fs.createReadStream(parts[i - 1]);
  upload = await uploadPart(file.id, fileStream, i);
}

// Complete the upload
upload = await completeMultiPartUpload(file.id);

// Clean up temporary files
for (const part of parts) {
  await fs.promises.unlink(part);
}
				
			

You can see how completeMultiPartUpload works here.

Note: these code snippets are just contrived examples. You’ll want to handle errors and upload failures in production code, including retries and potentially doing work in background jobs. You may want to consider being able to handle uploading multi-parts in any order and keeping track of state in a database so you can resume in case of issues.

4. Use the uploaded file

Finally we can use the file_upload to attach it to a property, page body, or as the cover of a Notion page. I’m thinking I might use this to create an image from a template for my daily journal!

				
					async function attachFileAsCover(upload, page) {
  return await notion.pages.update({
    page_id: page.id,
    cover: {
      type: 'file_upload',
      file_upload: {
        id: upload.id,
      },
    },
  });
}
				
			

Code examples

You can check out some of the code examples in the following pull request.

https://github.com/typeoneerror/notion-api-examples/pull/7/files

In the repository, you can find the examples in the examples/files folder. And you’ll probably be interested in the shared/files.js code.

I added some helpers for uploading and helpers for attaching, embedding, and adding page covers.

The PR includes:

  • Uploading a text file to a property and page body
  • Uploading a large video in multi-parts
  • Using an image as a page cover

This makes it much easier to get started with the new functionality.

Why it matters

Before this update, developers had to use workarounds to get files into Notion through the API:

  1. Host files on an external service (S3, Dropbox, etc.)
  2. Generate a public URL for the file
  3. Use the Notion API to embed that URL

This multi-step process required additional services, added complexity, and often broke when external links expired. The new direct upload capability streamlines workflows dramatically.

Use Cases

This opens up many new automation possibilities:

  • Document Management – Automatically upload reports, invoices, or contracts to Notion databases
  • CRM Enhancements – Attach client files directly to Notion CRM entries
  • Content Pipelines – Upload images and videos directly into content management databases
  • Data Processing – Generate files programmatically and attach them to relevant Notion pages
  • Automated Backups – Create automated systems to back up important files to Notion
  • Knowledge Base Building – Upload documentation files directly to company wikis

Implementation Considerations

When building with the file upload API, keep these things in mind:

  • For files under 20MB, the process is simple enough for most automation platforms
  • For larger files, you’ll need to implement multi-part uploads (possibly better suited for custom code)
  • Consider rate limits and file size restrictions when designing your automation
  • Structure your code to handle upload failures gracefully

Will this mean no more manual uploading?

The file upload API could eliminate several manual tasks:

  • No more downloading files just to re-upload them to Notion
  • Automated file organization becomes possible
  • Files can be attached to their relevant context without human intervention
  • Media processing workflows can now include Notion as a destination

It won’t fully remove uploading though. There may be cases where there’s no way to programmatically get a file out of another platform.

Getting Started

If you want to dive into the new functionality, check out:

The examples provide practical demonstrations of both simple and complex upload scenarios, making it easier to implement in your own projects.

What are your thoughts?

How will you use the file upload API in your workflows? Are there manual steps this will eliminate for you?

Share This Post

Email
LinkedIn

Get 🔥 Notion tips in your inbox

When you sign up, you’ll get tips, perspectives, and opinions on how you can better use Notion. Plus you’ll get a steady drip of Notion updates, videos, interviews, and resources from the Notion Mastery team.

Master your life and business workflows with Notion.