Uploads
POST /v1/uploads mints a presigned S3 PUT URL for a single file.
This is the entry point for any endpoint that takes an uploadId:
POST /v1/media (an image for a post), POST /v1/sources with
type: "file" and POST /v1/knowledge with type: "file".
The handle is not a media key. PATCH /v1/posts/{postId}/media
rejects it — turn the uploaded image into a library asset with
POST /v1/media first, and attach the img_… id that returns.
The flow is two-step on purpose:
POST /v1/uploads→ returns{ id: "upl_...", uploadUrl, ... }.PUT <uploadUrl>with your file bytes (no auth needed; the signed URL carries it). The S3 service receives the bytes directly.POST /v1/media/POST /v1/sources/POST /v1/knowledgewithuploadId: "upl_...".
- File payloads bypass the API edge entirely — Vercel and Cloudflare function tiers cap request bodies at ~4 MB, so a single-shot upload would not work for podcasts or PDFs.
- The content type is part of the signature, so S3 rejects a PUT that sends a different one.
- The PUT can be resumed (S3 multi-part) without coordinating with Scripe’s API.
uploadId is opaque — treat it as a reference, don’t
parse it. It’s bound to your workspace; another workspace can’t use it
even if they guess it.
MCP hosts don’t need this flow for small files (≤ ~3 MB decoded) — thecreate_source_fileandadd_to_knowledge_basetools accept file bytes inline as base64. Anything larger MUST come through this flow: bigger inline payloads are rejected at the platform edge as a bare HTTP 413.
POST /v1/uploads
Required scope: any of sources:write, knowledge:write (we accept
either since the same upload can be used for both endpoints).
Request body
Allowed content types and size caps
The families above are the whole allow-list. Notably
text/markdown
and text/html are not on it — sign them as text/plain, or
create the source through POST /v1/sources instead.
The cap is not signed into the presigned URL — AWS has no “at most
N bytes” condition for a PutObject signature. An oversized PUT is
therefore not guaranteed to fail fast: a bucket policy may deny it at
the S3 edge (a 403), and if it gets through, the object is rejected
later when the ingest worker checks its size, surfacing as a failed job
rather than a failed upload. Stay under the cap yourself — compress,
transcode, or chunk at your end (the dashboard does this for video
clips).
If you supply a contentType that isn’t in the table, the call fails
with 422 unprocessable.
Response
Semantics
The ticket’s fields are in the OpenAPI reference tab (Upload). The
sharp edges it doesn’t carry:
idis the handle you pass asuploadId— toPOST /v1/media,POST /v1/sources, orPOST /v1/knowledge.- Do not send an
Authorizationheader on the PUT — the signature is already inuploadUrl, and an extra header breaks it. contentTypeandmaxSizeBytesecho what you signed for.maxSizeBytesis not enforced by the signature; see the caps section above for where it actually is enforced.idisupl_<workspaceId>--<uploadId>. Both halves are bare ids with no prefix of their own: the workspace half is the internal 16-hex id for an API-key caller or anorg_…id for an OAuth caller, and the upload half is a 16-hex id. Treat the whole string as opaque rather than splitting it.- Past
expiresAtthe URL fails the signature check rather than returning a friendly error.
Doing the PUT
- The
Content-Typeheader on the PUT must matchcontentTypefrom the response — S3 enforces it as part of the signature. - The body must be the raw file bytes. No multipart/form-data wrapping.
- A successful PUT returns
200 OKwith an empty body. - A 403 with
SignatureDoesNotMatchusually means theContent-Typeheader diverged or the URL was URL-decoded somewhere in your client.
Errors
What’s NOT here (yet)
- Listing or deleting upload handles. Handles expire after 15 minutes if not redeemed, and a successful ingest job deletes the underlying S3 object. There’s no surface to list pending uploads — treat them as fire-and-forget.
- Resumable / multi-part uploads. Use the underlying AWS multi-part signing if your client library supports it; the URL we return is still a single-PUT signed URL.
- GET on uploads. We do not expose the file once it’s uploaded; the
download path is through the resource that consumed it (
Sourcesurfaces a transcript preview,KnowledgeDocumentsurfaces summary metadata).