PortModels
Log in

File storage

A per-user file folder scoped to your app, including presigned uploads for large files.

Each user gets a private folder per app, backed by S3-compatible storage under:

text
users/{user}/apps/{your app}/...

Requires the files.read and files.write scopes.

Upload#

The body is the raw file — not JSON, not multipart:

bash
curl -X PUT https://api.portmodels.com/connect/files/notes/2026-07.md \
  -H "Authorization: Bearer pmc_..." \
  -H "Content-Type: text/markdown" \
  --data-binary @notes.md

Direct uploads through the API are capped at 10MB. Larger files return 413 — use a presigned upload.

Download#

bash
curl https://api.portmodels.com/connect/files/notes/2026-07.md \
  -H "Authorization: Bearer pmc_..."

List#

bash
curl "https://api.portmodels.com/connect/files?prefix=notes" \
  -H "Authorization: Bearer pmc_..."
json
{ "files": [ { "path": "notes/2026-07.md", "size": 4821 } ] }

prefix is optional and filters within the user's folder for your app.

Delete#

bash
curl -X DELETE https://api.portmodels.com/connect/files/notes/2026-07.md \
  -H "Authorization: Bearer pmc_..."

Paths#

Paths are relative to the user's folder for your app. .. traversal is rejected — you cannot reach another app's folder or another user's data this way.

Use / to organize:

text
notes/2026-07.md
exports/report-final.pdf
cache/embeddings.json

Large files: presigned URLs#

For files over the 10MB proxy limit, ask for a time-limited URL and upload directly to storage, bypassing the API server:

bash
# Ask for the URL — content_length is required for the quota check
curl "https://api.portmodels.com/connect/files/presign-upload/exports/video.mp4?content_length=52428800" \
  -H "Authorization: Bearer pmc_..."

Then PUT the bytes to the returned URL. Downloads have the same shortcut:

bash
curl "https://api.portmodels.com/connect/files/presign-download/exports/video.mp4" \
  -H "Authorization: Bearer pmc_..."

Presigned URLs are time-limited. Generate one when you need it rather than storing it.

Note

Presigned URLs require a real S3 endpoint. A development server running the in-memory storage backend returns 501 for these routes — use the direct PUT path in development.

Storage quota#

Private-folder writes are metered against the connected user's storage subscription. Once a write would take the user past their quota, the request returns 402 with a storage quota exceeded error. Surface this to the user as "upgrade or free up space", not as a generic failure.

See App Data Layer plans.

Cross-app folders#

An app can be granted access to another app's folder for the same user through the concrete scopes files.read:{app_id} and files.write:{app_id}, using:

text
GET|PUT|DELETE /connect/files/apps/{app_id}/{path}

These scopes are only ever grantable between apps owned by the same developer, and the user still has to approve them. Use this when you ship a suite of apps that share output — not as a way around isolation.