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
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:
- Host files on an external service (S3, Dropbox, etc.)
- Generate a public URL for the file
- 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?