Learn how to send and attach files up to 20 MB using the Notion API.
The Direct Upload method lets you securely upload private files to Notion-managed storage via the API. Once uploaded, these files can be reused and attached to pages, blocks, or database properties.This guide walks you through the upload lifecycle:
1
Create a file upload object
2
Send the file content to Notion
3
Attach the file to content in your workspace
Tip:Upload once, attach many times. You can reuse the same file_upload ID across multiple blocks or pages.
Next, use the upload_url or File Upload object id from Step 1 to send the binary file contents to Notion.
Tips:
The only required field is the file contents under the file key.
Unlike other Notion APIs, the Send File Upload endpoint expects a Content-Type of multipart/form-data, not application/json.
Include a boundary in the Content-Type header [for the Send File Upload API] as described in RFC 2388 and RFC 1341. Most HTTP clients (e.g. fetch, ky) handle this automatically if you include FormData with your file and don’t pass an explicit Content-Type header.
// Open a read stream for the fileconst fileStream = fs.createReadStream(filePath)// Create form data with the (named) file contents under the `file` key.const form = new FormData()form.append('file', fileStream, { filename: path.basename(filePath)})// HTTP POST to the Send File Upload API.const response = await fetch( `https://api.notion.com/v1/file_uploads/${fileUploadId}/send`, { method: 'POST', body: form, headers: { 'Authorization': `Bearer ${notionToken}`, 'Notion-Version': notionVersion, } })// Rescue validation errors. Possible HTTP 400 cases include:// - content length greater than the 20MB limit// - FileUpload not in the `pending` status (e.g. `expired`)// - invalid or unsupported file content typeif (!response.ok) { const errorBody = await response.text() console.log('Error response body:', errorBody) throw new Error(`HTTP error with status: ${response.status}`)}const data = await response.json()// ...
file_name = "test.png"with open(file_name, "rb") as f: # Provide the MIME content type of the file as the 3rd argument. files = { "file": (file_name, f, "image/png") } response = requests.post( f"https://api.notion.com/v1/file_uploads/{file_upload_id}/send", headers={ "Authorization": f"Bearer {NOTION_KEY}", "Notion-Version": "2026-03-11" }, files=files ) if response.status_code != 200: raise Exception( f"File upload failed with status code {response.status_code}: {response.text}")
Once the file’s status is uploaded, it can be attached to any location that supports file objects using the File Upload object id.This step uses standard Notion API endpoints; there’s no special upload-specific API for attaching. Just pass a file object with a type of file_upload and include the id that you received earlier in Step 1.You can use the file upload id with the following APIs:
When a file is first uploaded, it has an expiry_time, one hour from the time of creation, during which it must be attached.Once attached to any page, block, or database in your workspace:
The expiry_time is removed.
The file becomes a permanent part of your workspace.
The status remains uploaded.
Even if the original content is deleted, the file_upload ID remains valid and can be reused to attach the file again.Currently, there is no way to delete or revoke a file upload after it has been created.
Attaching a file upload gives you access to a temporary download URL via the Notion API.These URLs expire after 1 hour.To refresh access, re-fetch the page, block, or database where the file is attached.
Tip:A file becomes persistent and reusable after the first successful attachment — no need to re-upload.