Meet the new Scripe, live on October 7.Register

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:

  1. POST /v1/uploads → returns { id: "upl_...", uploadUrl, ... }.
  2. PUT <uploadUrl> with your file bytes (no auth needed; the signed URL carries it). The S3 service receives the bytes directly.
  3. POST /v1/media / POST /v1/sources / POST /v1/knowledge with uploadId: "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_file and add_to_knowledge_base tools 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).

bash
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

FieldTypeRequiredNotes
contentTypestringyesThe MIME type you'll PUT. Must be one of the allow-listed types below.
maxSizeBytesintegernoThe 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

FamilyMatchesCap per object
Audioany audio/* — audio/mpeg, audio/wav, audio/mp4500 MB
Videoany video/* — video/mp4, video/quicktime500 MB
Imagesany image/* — image/png, image/jpeg25 MB
PDFapplication/pdf100 MB
Office documentsapplication/msword, application/vnd.openxmlformats-officedocument.* (e.g. …wordprocessingml.document), application/vnd.ms-*100 MB
Plain texttext/plain25 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
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:

  • id is the handle you pass as uploadId — to POST /v1/media, POST /v1/sources, or POST /v1/knowledge.
  • Do not send an Authorization header on the PUT — the signature is already in uploadUrl, and an extra header breaks it.
  • contentType and maxSizeBytes echo what you signed for. maxSizeBytes is not enforced by the signature; see the caps section above for where it actually is enforced.
  • id is upl_<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 an org_… 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 expiresAt the URL fails the signature check rather than returning a friendly error.

Doing the PUT

bash
curl -i -X PUT "$UPLOAD_URL" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @./episode-12.mp3

Notes:

  • The Content-Type header on the PUT must match contentType from 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 OK with an empty body.
  • A 403 with SignatureDoesNotMatch usually means the Content-Type header diverged or the URL was URL-decoded somewhere in your client.

Errors

StatusCode
400invalid_request (missing contentType)
401unauthenticated, invalid_token, key_revoked, key_expired
403scope_missing (need sources:write or knowledge:write)
422unprocessable (unsupported contentType, maxSizeBytes over cap or non-positive)
429rate_limited
503service_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 (Source surfaces a transcript preview, KnowledgeDocument surfaces summary metadata).