Posts
Upload media via signed URL
Receive an MCP-issued one-shot signed URL upload. Normally invoked by the request-media-upload-tool flow, not called directly.
POST
This endpoint is the destination of a signed POST URL issued by the MCP
request-media-upload-tool. It exists to let AI agents and their clients upload local files to your workspace without provisioning a Personal Access Token — the signed URL itself is the auth.
You normally don’t call this endpoint directly. The flow is:
- AI agent calls
request-media-upload-tool→ receivesupload_url+upload_token(+ size caps) - The agent (or the user) POSTs the file to
upload_urlas multipart fieldmedia - AI agent calls
attach-media-from-upload-toolwith the sameupload_token(and optionalaltfor images) to attach the resulting Media to a post
POST /posts/{post}/media — direct upload to a specific post, no signed URL. To reuse a file already in the Asset Library, use POST /posts/{post}/media/from-asset instead of uploading again.
Path parameter
string
required
The single-use UUID baked into the signed URL by
request-media-upload-tool. Must be a valid UUID; non-UUID values 404.Query parameters (added by the signed URL)
These are added to the URL by Laravel’sURL::temporarySignedRoute and are covered by the HMAC signature — never set them yourself.
string
required
The workspace this upload belongs to. Tampering with this value invalidates the signature and the endpoint returns
403.integer
required
Unix timestamp when the URL expires. Default TTL is 15 minutes from issuance (configurable via
MEDIA_SIGNED_UPLOAD_URL_TTL_MINUTES, legacy fallback MCP_UPLOAD_URL_TTL_MINUTES).string
required
HMAC-SHA256 of the URL + query parameters, signed with the app’s
APP_KEY. Verified by Laravel’s signed middleware before the controller runs.Request
Send asmultipart/form-data with a single media field.
file
required
The file to upload. Per-type size caps match the rest of TryPost (from
trypost.media.max_size_mb / env):The FormRequest upper bound is the video ceiling; the per-type cap is enforced after MIME detection. MIME is detected from the file’s magic bytes (not the extension or
Content-Type header).request-media-upload-tool also returns these limits as max_bytes (video ceiling) and max_bytes_by_type.Authentication
No Bearer token. The signed URL is the credential.- The
signedmiddleware verifies the HMAC againstAPP_KEYand rejects expired URLs with403. - An atomic
Cache::addplus aUNIQUEconstraint onmedia.upload_tokenensure the URL is single-use: the second POST with the sametokenreturns409. - Rate-limited per workspace and per IP (
MEDIA_SIGNED_UPLOAD_PER_WORKSPACE_PER_MINUTE/MEDIA_SIGNED_UPLOAD_PER_IP_PER_MINUTE).
Behaviour
- The file is streamed to the workspace’s configured filesystem disk (
local,s3,r2, etc.) atmedias/{uuid}.{ext}— not loaded fully into memory (videos can be up to 1 GB). The path is generated server-side — client filenames are never used in the path. - PNG and WebP still images are normalized to JPEG (q100) for universal platform compatibility. GIF is preserved (animation kept for X / Bluesky / Mastodon / Telegram / Discord). Videos and PDFs are stored as-is.
- The resulting
Mediarow is tagged with theupload_tokenso the AI agent can reference it later viaattach-media-from-upload-tool. The Media is created as a workspace asset — not yet attached to any post. - This endpoint is not triggered by the
POST /posts/{post}/mediaflow. It only handles MCP-issued signed URLs.
Response
string
Echo of the
{token} path parameter, for client convenience.string
The UUID of the created
Media record. Pass the upload_token to attach-media-from-upload-tool to attach to a post.string
image, video, or document.string
The detected MIME type. Note that PNG/WebP inputs are reported as
image/jpeg due to the normalization step.string
The filename the client sent (stored as metadata, never used for the storage path).

