Upload a media asset
Stores one file against the project, registers it with Meta for sending, and returns the slug your bot resolves it by at runtime (sendMedia(to, "<slug>") / getMediaId("<slug>")). Write the slug into a table row with POST /v1/tables/{table}/rows and the bot can send the asset later without ever touching the dashboard.
Three transports, pick one. Every one of them needs the file itself:
multipart/form-datawith a single part namedfile, plus optionalnameandcaptiontext fields. Up to 20 MB.- JSON
{ "source_url": "https://..." }— Paige downloads it for you. Up to 20 MB. Only public http(s) URLs: private, loopback, link-local and other reserved addresses are refused (400 blocked_source_url), redirects are re-checked at every hop and capped, and a non-http(s) scheme is400 invalid_source_url. Addfilename/mimeto override what the remote server declares. - JSON
{ "data": "<base64>", "filename": "...", "mime": "..." }— inline bytes, capped at 8 MB decoded, lower than the other two on purpose: base64 costs about 4/3 in transit and is buffered whole in memory, so large files belong on multipart orsource_url. Adata:URI prefix is accepted and stripped.
name is the slug. Send it to choose the bot-facing name; omit it and the filename is used. It is normalized to lowercase letters, digits and hyphens. A slug that already exists is 409 SLUG_EXISTS — it is never overwritten, because the existing asset may already be referenced by deployed code or a published flow.
Meta registration is best-effort, and the response tells you the truth about it. media_id is null and meta_status is "pending" when the file was stored but not registered (typically because no WhatsApp number is connected). The asset is still usable and Paige retries registration on a schedule; a failed register is never reported as a success. meta_status is "expired" once the id passes Meta’s 30-day lifetime.
Size and type. Oversize is 413 file_too_large; a type Paige cannot ingest is 415; an empty file is 400. Images, video, audio, PDF, DOCX, plain text and markdown are accepted.
Uploading an image costs credits. Paige runs one AI vision call per image to write the knowledge description your bot answers from, and that call is billed to the project like any other AI usage — it shows up in Usage alongside builds. Out of credits is 402 insufficient_credits. Send describe_image: false to skip it: the file is still stored, still registered with Meta and still usable, it just gets no AI-written description and costs nothing. Non-image uploads (PDF, DOCX, text) use no AI at all and are always free.
Two limits apply beyond the per-request throttle. Uploads are metered by BYTES as well as by request count, so a burst of large files can return 429 rate_limited even while you are inside the request-per-minute budget — retry after the window or upload in smaller batches. And each project has a total media storage ceiling: an upload that would exceed it is refused with 413 storage_limit_exceeded rather than quietly succeeding. GET /v1/media reports storage.used_bytes / storage.remaining_bytes so you can check before you push.
Send an Idempotency-Key header to make a retry safe. The fingerprint covers the file bytes, so retrying the same upload replays the original response, while reusing the key for a different file is 409 idempotency_key_mismatch rather than a silently-dropped upload.
Required scope: media:write
Authorizations
Project API key issued in Settings → API keys. Send it as Authorization: Bearer pk_live_….
Headers
Target project id, for an MCP OAuth bearer (mcp_at_…) attached to more than one project — get ids from GET /v1/projects. Matched case-insensitively.
Omit it and a READ falls back to the connection's default project; a mutation (any non-GET) on a connection with 2+ projects is rejected with 400 project_required — a write is never defaulted to a guessed project. A connection with exactly one project never needs the header.
Scopes are checked against the SELECTED project only, never a union across the connection.
For a pk_ API key the header selects nothing — one key is one project's context — but it IS validated: omit it and the key's own project is used, send it and it must name that project, otherwise the call is rejected with 403 project_not_attached (a blank value is 400 invalid_project_header, as above).
Body
- Option 1
- Option 2
Public http(s) URL Paige downloads the file from. Private, loopback and link-local addresses are refused, redirects are re-checked at every hop.
Optional bot-facing name for the asset. Normalized to a slug; blank falls back to the filename.
2000Overrides the filename derived from the URL. The slug still comes from name when you send one.
1 - 255Overrides the server's Content-Type. Use it when the host serves the file as application/octet-stream.
1 - 255Run the AI vision description on an image upload (default true). It costs credits, so set false to store and register the file without it. Ignored for non-image files. Accepts a boolean, or true/false/1/0 as a multipart form field.
