Uploads

Presign a large media upload

Creates presigned URLs so large media is uploaded straight to object storage instead of through this API.

POST /upload accepts the file itself, but its request body is capped at 100 MB by our edge: a bigger multipart request is rejected with 413 before it ever reaches the API. That ceiling is an edge limit, not a platform one. The real per-media ceilings are the maxFileSize of the presign route you choose (up to 10 GiB) and, at publication time, the target platform's own limits.

Nothing changes for files under 100 MB: keep sending them to POST /upload as a file part exactly as before. Presign only what is too big for that path.

Phase 1 - presign

Declare each file's exact byte length; it is signed into the URLs you get back.

curl -X POST "https://api.multi-upload-tool.com/api/v1/upload/presign" \
  -H "x-api-key: kmb_YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"route":"tiktok_videos","files":[{"name":"clip.mp4","size":734003200,"type":"video/mp4"}]}'

Phase 2 - upload the bytes

They travel directly to storage and never touch this API or its edge, which is what removes the 100 MB ceiling.

Single-request routes (photos, instagram_photos, threads_photos, x_photos, bluesky_photos, thumbnails, documents) return uploadUrl and uploadHeaders. PUT the whole file to uploadUrl, sending exactly the headers returned: content-type and content-length are part of the signature, so changing or omitting either fails the PUT with SignatureDoesNotMatch. Valid for 1 hour.

Multipart routes (youtube_videos, tiktok_videos, pinterest_videos, threads_videos, x_videos, linkedin_videos) return multipart. For each entry of multipart.parts, PUT the byte range starting at (partNumber - 1) * partSize and size bytes long to that part's uploadUrl, with Content-Length equal to that part's size. Keep each response's ETag header, quotes stripped. Parts may be uploaded in parallel, and a single failed part can be retried by re-PUTting the same URL. Once every part is stored, POST to multipart.completeUrl with Content-Type: application/xml and the parts sorted ascending:

<CompleteMultipartUpload><Part><ETag>abc123</ETag><PartNumber>1</PartNumber></Part><Part><ETag>def456</ETag><PartNumber>2</PartNumber></Part></CompleteMultipartUpload>

If you abandon the upload, send DELETE to multipart.abortUrl: parts left behind are stored and billed and are not cleaned up for you. Part and complete URLs last 6 hours and cannot be refreshed, so upload parts concurrently rather than one after another, and presign again if the window lapses.

Phase 3 - publish

Each presigned file comes back with two URLs for the same object. Pass one of them as the video, photo or file field of POST /upload - those fields already accept a URL string - or of POST /upload/bulk to fan the same media out across accounts.

  • bucketUrl - use it for large video. The API reads the object straight from storage, so the file is never streamed back through this API and its edge on the way to the platform. It points at private storage and is not fetchable on its own.
  • accessUrl - use it when you also need a URL anyone can fetch, and for photos. It is served from a domain the platforms have verified, which is what lets TikTok and the other pull-based platforms fetch the media. Pass it through unchanged; a rehosted copy on your own domain is rejected by those platforms.
curl -X POST "https://api.multi-upload-tool.com/api/v1/upload" \
  -H "x-api-key: kmb_YOUR_TOKEN" \
  -F "accountId=101" \
  -F "video=https://multi-upload-tools.s3.eu-central-003.backblazeb2.com/public-api/42/1756226400000-a1b2c3d4-clip.mp4" \
  -F "title=My Video"

Routes

route selects the validation preset applied to the files you declare. It does not tie the media to a platform - any presigned object may be published to any account - but choosing the preset that matches your target rejects an oversized or wrong-typed file here, instead of at publication time where the platform's own limits still apply.

routemultipartmax filesmax size per fileaccepted types
youtube_videosyes110 GiBvideo/*, application/octet-stream
tiktok_videosyes14 GiBvideo/mp4, video/webm
pinterest_videosyes12 GiBvideo/mp4, video/quicktime, video/x-m4v
threads_videosyes11 GiBvideo/mp4, video/quicktime
x_videosyes11 GiBvideo/mp4, video/quicktime
linkedin_videosyes1500 MiBvideo/mp4
documentsno1100 MiBPDF, PPT, PPTX, DOC, DOCX
photosno3520 MiBimage/jpeg, image/png, image/webp
thumbnailsno120 MiBimage/jpeg, image/png, image/webp, image/gif
instagram_photosno108 MiBimage/jpeg, image/png, image/webp
threads_photosno208 MiBimage/jpeg, image/png
x_photosno45 MiBimage/jpeg, image/png, image/webp, image/gif
bluesky_photosno42 000 000 bytesimage/jpeg, image/png, image/webp, image/gif

A declared size of 0 is rejected: a zero-byte multipart upload can never be completed.

Quota and rate limit

Presigning is metered per user rather than per IP, so a fleet of workers behind one egress address is not throttled as if it were one client, and one busy token cannot spend another customer's allowance. The limit is 120 requests / 15 min; exceeding it returns 429 with retryAfter in seconds. A far looser per-IP backstop of 600 requests / 15 min also applies, and answers 429 the same way.

The request body only declares files, so it is capped at 256 KB - a bigger one is refused with 413 and code: BODY_TOO_LARGE without being read.

The bytes you declare are charged against a rolling 30-day allowance that depends on your plan. Every successful response carries a usage block - what is used including this request, the allowance, and what is left - so you can see the headroom without failing first. Exceeding it returns 403 with code: STORAGE_QUOTA_EXCEEDED. The debit happens when the URLs are issued, using the sizes you declared, and is not refunded if you never upload the bytes: presigning and walking away spends the allowance. Plans with no configured allowance report "unlimited": true and are never blocked.

post/upload/presign

Request

  • Base URL: https://api.multi-upload-tool.com/api/v1
  • URL: https://api.multi-upload-tool.com/api/v1/upload/presign
  • Auth: API key in header x-api-key

Headers

X-Team-IDinteger

Act inside this team. Ignored when the token is already pinned to a team; membership is verified on every request.

Request body

route'youtube_videos' | 'tiktok_videos' | 'pinterest_videos' | 'threads_videos' | 'x_videos' | 'linkedin_videos' | 'documents' | 'photos' | 'thumbnails' | 'instagram_photos' | 'threads_photos' | 'x_photos' | 'bluesky_photos' required

Validation preset applied to the declared files: it fixes the accepted MIME types, the maximum number of files, the maximum size per file, and whether the upload is multipart. It does not tie the media to a platform.

metadataobject

Accepted and ignored, so bodies produced by generic presigned-upload clients validate.

Example request

{
  "files": [
    {
      "name": "clip.mp4",
      "size": 734003200,
      "type": "video/mp4"
    }
  ]
}

Response

One presigned entry per declared file, plus the caller's current storage usage.

successunknown required

Changes

No recorded changes to this operation across all 1 revision of this API.