POST/v1/media

Upload media

Returns a presigned URL you can PUTa file to, plus the public URL of the resulting object. Use this when you don't want to host the media yourself, or when you need the file to stay reachable for scheduled posts where the window between the API call and the actual publish can span hours or days.

You don't have to use this endpoint. If you already host your media somewhere publicly accessible, you can pass that URL straight into the media array on POST /v1/posts— Synposter will fetch it for you. This endpoint exists for the case where hosting your own media isn't convenient.

How it works

  1. Call POST /v1/media with the filename and MIME type. We respond with an upload_url (a short-lived presigned URL), a public_url (where the file will live), and an expires_at timestamp.
  2. PUT the raw file bytes to upload_url, with the same Content-Type you declared in step 1. No auth header needed — the URL itself is the authorization.
  3. Reference public_url in any subsequent POST /v1/posts call: media: [{ type: "image", url: public_url }].
curl -X POST https://api.synposter.com/v1/media \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "launch-banner.png",
    "mime_type": "image/png"
  }'

Body

FieldTypeRequiredDescription
filenamestringYesThe original filename, used to derive the file extension on the stored object so the public URL is sensibly named. 1 to 255 characters; must not contain path separators (/, \) or null bytes.
mime_typestringYesThe MIME type of the file you're about to upload, e.g. "image/png" or "video/mp4". Only image/* and video/* are accepted. The same value must be sent as the Content-Type header on the subsequent PUT to upload_url.

Response (201)

json
{
  "id": "9b3a1f2c-...",
  "upload_url": "https://<project>.supabase.co/storage/v1/object/upload/sign/media/...",
  "public_url": "https://<project>.supabase.co/storage/v1/object/public/media/<prefix>/<uuid>.png",
  "path": "<prefix>/<uuid>.png",
  "mime_type": "image/png",
  "expires_at": "2026-04-29T16:00:00Z"
}
FieldTypeRequiredDescription
idstringYesSynposter's UUID for this upload record. Stored in our audit log so the upload can be retrieved or deleted later by id (those endpoints will ship alongside scheduled posts).
upload_urlstringYesShort-lived presigned URL. PUT your file bytes here within the expires_at window. Bearer / API-key auth is not required on this PUT — the URL itself is the credential.
public_urlstringYesWhere the file will be readable from once the upload completes. Use this value in the media array on POST /v1/posts. Note: the URL works only after a successful PUT; before that it returns 404.
pathstringYesThe internal storage path for the object — an opaque per-developer prefix followed by a UUID. Useful if you need to reference or delete the object later via Synposter's admin tooling.
mime_typestringYesEchoed back from your request. Send the exact same value as the Content-Type header on the PUT.
expires_atstringYesISO 8601 timestamp after which upload_url stops working. Currently 2 hours from the moment the URL is issued.

Example: full upload + post

bash
# 1. Get a presigned URL.
RESP=$(curl -s -X POST https://api.synposter.com/v1/media \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "cat.png", "mime_type": "image/png" }')

UPLOAD_URL=$(echo "$RESP" | jq -r .upload_url)
PUBLIC_URL=$(echo "$RESP" | jq -r .public_url)

# 2. PUT the file to the presigned URL.
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/png" \
  --data-binary @./cat.png

# 3. Reference the public URL in a post.
curl -X POST https://api.synposter.com/v1/posts \
  -H "x-api-key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"accounts\": [\"<account_id>\"],
    \"text\": \"Hello with image!\",
    \"media\": [{ \"type\": \"image\", \"url\": \"$PUBLIC_URL\" }]
  }"

Limits

  • Max file size: 50MB per object. The upload itself is rejected at the storage layer if you exceed this — your PUT returns an error and nothing is stored.
  • Allowed MIME types: anything starting with image/ or video/. Other types (PDFs, audio, etc.) are rejected with invalid_mime_type.
  • URL TTL: upload_url is valid for 2 hours from issuance. The public_url never expires (the file stays available as long as the object exists).
  • Per-platform constraints still apply when you reference the URL in POST /v1/posts — for example, X enforces a 5MB cap on images and a 15MB cap on GIFs even though the upload itself succeeded at the larger bucket limit.

Status codes

201The presigned URL was created and is returned in the response body.
400Returned with one of invalid_json (the request body wasn't valid JSON or didn't parse to an object), invalid_filename (missing, empty, too long, or contained / \ \0), or invalid_mime_type (missing or not image/* or video/*).
401The API key was missing or invalid.
500Something failed on Synposter's side while signing the upload URL. Safe to retry.