Notion MCP Cannot Upload Files? Use the Native File Upload API

Notion MCP Cannot Upload Files? Use the Native File Upload API

Aident AI

A coral capsule passes through a mint portal and emerges cyan, representing a private file crossing the native upload boundary.

Notion MCP Cannot Upload Files? Use the Native File Upload API

Notion's hosted MCP server cannot currently upload images or files. Notion documents that limitation explicitly and recommends its File Upload API as the interim path. The reliable fix is a three-step flow: create a File Upload object, send the file bytes, then attach the returned file_upload ID to a page, block, cover, icon, or database files property.

Do not base64-encode a PDF into Markdown or make a private receipt public just to give a connector a URL. Use an agent integration that exposes Notion's native upload actions, or call the File Upload API from a runtime that can reach api.notion.com.

Confirm the Connector Is the Boundary

First inspect the tools your agent can actually call. A file-related tool that accepts only inline UTF-8 content or a public source_url is not the same as Notion's native File Upload API.

The native flow needs all three capabilities:

  1. Create a File Upload object and receive an id and upload destination.

  2. Send binary file contents to that upload.

  3. Reference the uploaded file by ID when creating or updating Notion content.

If step 1 or 2 is missing from the tool schema, changing your prompt will not add binary transport. This is a capability boundary, not a wording problem.

Prerequisites

Before uploading, confirm:

  • The Notion connection can edit the destination page or database.

  • You know the destination page or block ID.

  • The file has a supported extension and correct MIME type.

  • A single-part upload fits Notion's 20 MB API limit and your workspace's file-size limit.

  • You can complete the upload and attach it within one hour.

For direct API calls, keep the Notion token in an environment variable. Do not paste it into a prompt, shell history, source file, or shared MCP configuration.

Upload a File Through an AI Agent

Aident Loadout gives Claude Code, Codex, and other agents a way to discover the current Notion action contract before a write. Install or update it by giving your agent this exact instruction:

Follow https://aident.ai/SETUP.md

Then give the agent the file as an attachment and use a bounded prompt:

Connect my Notion workspace, then find the Notion actions for creating and
sending a file upload. Inspect both action schemas before writing anything.

For the attached report.pdf:
1. Create a single-part upload with filename report.pdf and MIME type application/pdf.
2. Send the attached file to the returned upload ID.
3. Confirm the upload status is uploaded.
4. Ask for approval, then append it as a file block to the Notion page I provide.
5. Read the page back and confirm the attached filename.

Do not create a public copy of the file and do not log credentials

Expected result: the create action returns a pending upload ID, the send action changes its status to uploaded, and the final Notion write references that same ID. The page readback should show report.pdf as a Notion-hosted file.

If your current agent surface cannot pass an attached local file into the send action, use a protected short-lived file-transfer action rather than exposing a caller-local path to a remote runtime. Delete the temporary copy after Notion has imported and attached the file.

Reproduce the Native Flow With cURL

Use this direct API path when your runtime can reach Notion and you manage the integration token yourself. It follows Notion's current 2026-03-11 API version.

Set the inputs without printing the token:

export NOTION_TOKEN="your_token_from_a_secret_manager"
export FILE_PATH="./report.pdf"
export FILE_NAME="report.pdf"
export MIME_TYPE="application/pdf"
export PAGE_ID="your_destination_page_id"

1. Create the upload

FILE_UPLOAD_ID="$({
  curl --fail --silent --show-error \
    --request POST \
    --url 'https://api.notion.com/v1/file_uploads' \
    --header "Authorization: Bearer ${NOTION_TOKEN}" \
    --header 'Content-Type: application/json' \
    --header 'Notion-Version: 2026-03-11' \
    --data "{\"mode\":\"single_part\",\"filename\":\"${FILE_NAME}\",\"content_type\":\"${MIME_TYPE}\"}" \
  | jq -r '.id'
})"

test -n "$FILE_UPLOAD_ID" && test "$FILE_UPLOAD_ID" != "null"

Expected result: the final test exits successfully and FILE_UPLOAD_ID contains a UUID. The create response has status pending.

2. Send the bytes

curl --fail --silent --show-error \
  --request POST \
  --url "https://api.notion.com/v1/file_uploads/${FILE_UPLOAD_ID}/send" \
  --header "Authorization: Bearer ${NOTION_TOKEN}" \
  --header 'Notion-Version: 2026-03-11' \
  --form "file=@${FILE_PATH};type=${MIME_TYPE}" \
  | jq '{id, status, filename, content_type, content_length}'

Expected result:

{
  "id": "the-same-upload-id",
  "status": "uploaded",
  "filename": "report.pdf",
  "content_type": "application/pdf",
  "content_length": "a-positive-byte-count"
}

3. Attach the upload to a page

jq -n --arg id "$FILE_UPLOAD_ID" '{
  children: [{
    object: "block",
    type: "file",
    file: {
      type: "file_upload",
      file_upload: {id: $id}
    }
  }]
}' | curl --fail --silent --show-error \
  --request PATCH \
  --url "https://api.notion.com/v1/blocks/${PAGE_ID}/children" \
  --header "Authorization: Bearer ${NOTION_TOKEN}" \
  --header 'Content-Type: application/json' \
  --header 'Notion-Version: 2026-03-11' \
  --data @- \
  | jq '.results[] | {id, type}

Expected result: Notion returns a new block with type file. Open the destination page and confirm that the file renders and downloads. Once attached, the file becomes a persistent part of the workspace; future API reads return temporary download URLs that must be refreshed after they expire.

Attach the File to a Database Property Instead

A database page with a files property uses the same upload ID. Update the page and set the property to a file object with:

{
  "type": "file_upload",
  "file_upload": {
    "id": "the-upload-id"
  },
  "name": "report.pdf"
}

This is useful for expense receipts, contracts, creative assets, and support evidence. If you are building a wider Notion workflow, keep the upload as one bounded stage inside the automation rather than mixing file transport with every database write. The same separation improves Notion CRM and email automations.

Common Failure Modes

The connector accepts only content or source_url

You are on a connector surface that does not expose binary upload. Use an integration with native create and send actions, or move the call to a runtime that can reach the Notion API. A public URL import is appropriate only when the source is intentionally public and returns a direct file response without cookies, redirects, or private-network access.

cURL returns exit code 000

The runtime likely cannot reach api.notion.com. Confirm DNS and outbound HTTPS from that exact runtime. If a hosted agent sandbox blocks the destination, route the operation through a reviewed Notion action instead of weakening the sandbox.

The send step returns validation_error

Check the file size, extension, MIME type, and upload state. A single-part file must fit the applicable workspace limit and the API's 20 MB ceiling. Files above 20 MB require a paid Notion workspace, multi_part mode, 5-20 MB chunks, and a separate complete step before attachment.

The upload stays pending

Creating the File Upload object does not transmit the bytes. Call the send action or /send endpoint with multipart form data and keep the form field named file.

The upload expires before it appears on a page

Attach the uploaded ID within one hour of creation. If it has expired, create a new upload and repeat the send step. Do not reuse a pending or archived upload ID.

Notion returns 401 or 403

Reconnect the Notion account or verify the token, workspace, and destination permissions. The destination page must be shared with the integration. Keep the granted access as narrow as the workflow allows.

Why This Fix Works

The three stages separate transport from content placement:

create upload -> send bytes -> attach stable upload ID

Notion MCP currently omits the first two stages. A native integration or direct API call restores those stages without forcing binary data through a text field. The final page operation still uses Notion's ordinary block and page APIs, so one uploaded object can be reused across supported blocks or properties.

This is also a useful example of when an API is the right fallback for MCP: MCP standardizes the agent-facing connection, but the available tool set still determines which provider operations the agent can perform.

Ready to test the flow without exposing a private file? Set up Aident Loadout and inspect the Notion upload actions.

Sources

Review this article when Notion adds file uploads to its hosted MCP tool set, changes the File Upload API version or limits, or changes the create, send, complete, or attachment contracts.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

The one tool

for every tool

your agent needs.

Give any AI agent real capabilities in seconds. Connect 1,000+ tools once, skip the setup headache, and let your agents execute.