Skip to Content
SimulatorsNotionFile uploads

Simulators

File uploads

File uploads let a test attach a file to a page the way Notion’s API does: open an upload, send its bytes, then name the upload where the file belongs. The simulator keeps the bytes and serves them itself, so the whole round trip stays on your machine:

curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"notion_team_wiki"}'
curl -s "$RYSTIC_URL/v1/file_uploads" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"filename":"notes.txt","content_type":"text/plain"}' | jq -c '{id, status}'
# -> {"id":"46dc3409-450d-40f1-8000-000000000001","status":"pending"}
printf 'Brand notes\n' | curl -s "$RYSTIC_URL/v1/file_uploads/46dc3409-450d-40f1-8000-000000000001/send" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -F 'file=@-;filename=notes.txt;type=text/plain' | jq -c '{status, filename, content_length}'
# -> {"status":"uploaded","filename":"notes.txt","content_length":12}
curl -s -X PATCH "$RYSTIC_URL/v1/blocks/a1000000-0000-4000-8000-000000000101/children" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"children":[{"type":"file","file":{"type":"file_upload","file_upload":{"id":"46dc3409-450d-40f1-8000-000000000001"}}}]}' | jq -r '.results[0].file.file.url' | xargs curl -s
# -> Brand notes
curl -s -X POST "$RYSTIC_URL/v1/file_uploads/46dc3409-450d-40f1-8000-000000000001/complete" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' | jq -c '{status, code}'
# -> {"status":400,"code":"validation_error"}
curl -s "$RYSTIC_URL/v1/file_uploads" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' -H 'Content-Type: application/json' -d '{"mode":"external_url","external_url":"https://example.com/brand.pdf","filename":"brand.pdf"}' | jq -c '{status}'
# -> {"status":"pending"}
curl -s "$RYSTIC_URL/v1/file_uploads/46dc3409-450d-40f1-8000-000000000002" -H 'Authorization: Bearer secret_placeholder' -H 'Notion-Version: 2026-03-11' | jq -c '{status}'
# -> {"status":"uploaded"}

The upload lifecycle

POST /v1/file_uploads answers a pending upload with its upload_url, which points at the simulator. A single_part send carries the file’s bytes as multipart/form-data, leaves the upload uploaded and drops upload_url and complete_url from the upload and from every later read of it, since Notion includes them only while an upload is pending. POST /v1/file_uploads/{file_upload_id}/complete answers only a multi_part upload: completing a single_part upload answers 400 validation_error, “One or more parts must be uploaded before completing the file upload”.

GET /v1/file_uploads lists only the calling connection’s uploads, and GET /v1/file_uploads/{file_upload_id} reads one.

Attach and read back

An upload attached to a file, image, PDF, audio or video block, to a page icon or cover or to a files property reads back as {"type": "file", "file": {"url", "expiry_time"}}. The URL, under /_rystic/files/…, serves the bytes you sent until expiry_time, one hour after each read on the simulator’s clock, and then answers 403. Upload, served-file and task-status URLs lead back to the simulator; page, database and data source urls do not.

External URL uploads

An external_url upload is a placeholder. The simulator never fetches an external_url: the upload answers pending, reads uploaded on its first GET /v1/file_uploads/{file_upload_id}, and its file serves a short text/plain text naming the source URL, not the remote file.

Multi-part uploads

Creating a multi_part upload, sending its parts and completing it follow Notion’s published spec alone. Notion’s free tier caps a file at 5 MiB and refuses a multi_part create with 400 validation_error; the simulator creates it, as Notion does for a paid workspace. Check a flow that relies on files over 5 MiB against Notion itself.

Last updated on