Skip to main content
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:
  1. AI agent calls request-media-upload-tool → receives upload_url + upload_token (+ size caps)
  2. The agent (or the user) POSTs the file to upload_url as multipart field media
  3. AI agent calls attach-media-from-upload-tool with the same upload_token (and optional alt for images) to attach the resulting Media to a post
If you already have a Personal Access Token, the simpler endpoint is 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’s URL::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 as multipart/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 signed middleware verifies the HMAC against APP_KEY and rejects expired URLs with 403.
  • An atomic Cache::add plus a UNIQUE constraint on media.upload_token ensure the URL is single-use: the second POST with the same token returns 409.
  • 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.) at medias/{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 Media row is tagged with the upload_token so the AI agent can reference it later via attach-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}/media flow. 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).

Status codes