API v1 · Resources
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_...".
Why two steps:
- 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.
The returned 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) — the
create_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).
curl -i https://api.scripe.io/v1/uploads \
-X POST \
-H "Authorization: Bearer $SCRIPE_OAUTH_TOKEN" \
-H "Scripe-Api-Version: 2026-08-10" \
-H "Content-Type: application/json" \
-d '{
"contentType": "audio/mpeg",
"maxSizeBytes": 104857600
}'Request body
| Field | Type | Required | Notes |
|---|---|---|---|
contentType | string | yes | The MIME type you'll PUT. Must be one of the allow-listed types below. |
maxSizeBytes | integer | no | The size you commit to PUTting at most. Must be >= 1 and <= the per-type cap below. Defaults to the per-type cap. |
Allowed content types and size caps
| Family | Matches | Cap per object |
|---|---|---|
| Audio | any audio/* — audio/mpeg, audio/wav, audio/mp4 | 500 MB |
| Video | any video/* — video/mp4, video/quicktime | 500 MB |
| Images | any image/* — image/png, image/jpeg | 25 MB |
application/pdf | 100 MB | |
| Office documents | application/msword, application/vnd.openxmlformats-officedocument.* (e.g. …wordprocessingml.document), application/vnd.ms-* | 100 MB |
| Plain text | text/plain | 25 MB |
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
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"id": "upl_9f2c4b7ae1d06835--3f9a1c7e2b5d40a8",
"uploadUrl": "https://scripe-uploads.s3.amazonaws.com/api-uploads/9f2c4b7ae1d06835/3f9a1c7e2b5d40a8?X-Amz-Algorithm=…",
"method": "PUT",
"contentType": "audio/mpeg",
"maxSizeBytes": 104857600,
"expiresAt": "2026-05-15T08:36:00.000Z"
}
}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
curl -i -X PUT "$UPLOAD_URL" \
-H "Content-Type: audio/mpeg" \
--data-binary @./episode-12.mp3Notes:
- 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
| Status | Code |
|---|---|
| 400 | invalid_request (missing contentType) |
| 401 | unauthenticated, invalid_token, key_revoked, key_expired |
| 403 | scope_missing (need sources:write or knowledge:write) |
| 422 | unprocessable (unsupported contentType, maxSizeBytes over cap or non-positive) |
| 429 | rate_limited |
| 503 | service_unavailable (S3 misconfiguration on our side; see status page) |
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).