Guides

Media uploads

Get images and video into CuteDyno, and what each platform accepts.

CuteDyno does not accept file bytes through the API. You ask for a presigned URL, upload the file straight to storage, and then reference the resulting public URL in a post. That keeps large video uploads off the API path, where they would be slow and prone to timeouts.

The short version

import { readFile } from 'node:fs/promises';

const { publicUrl } = await cutedyno.media.upload({
  fileName: 'launch.mp4',
  fileType: 'video/mp4',
  data: await readFile('./launch.mp4'),
});

await cutedyno.posts.create({
  content: 'Six months of work, live today.',
  media: [{ type: 'video', url: publicUrl }],
  accountIds: ['3f2a…'],
  publishNow: true,
});

media.upload does the presign and the transfer in one call. Use it unless you need to manage the transfer yourself — a browser upload with a progress bar, or a resumable upload for a large file on a bad connection.

Doing it manually

Two steps. First, presign:

curl https://api.cutedyno.com/v1/media/upload \
  -H "Authorization: Bearer $CUTEDYNO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "fileName": "launch.mp4", "fileType": "video/mp4" }'
{
  "signedUrl": "https://storage.cutedyno.com/…?X-Amz-Signature=…",
  "publicUrl": "https://storage.cutedyno.com/workspace-…/videos/1754049600000-….mp4",
  "key": "workspace-…/videos/1754049600000-….mp4",
  "expiresIn": 3600,
  "maxBytes": 104857600
}

Then PUT the bytes, with the same Content-Type you presigned with:

curl -X PUT "$SIGNED_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @launch.mp4

A mismatched Content-Type fails the signature check, which shows up as a 403 from storage rather than an error from CuteDyno. If a PUT is rejected, check that header first.

publicUrl works as soon as the PUT returns. You do not need to wait or poll.

Uploading from a browser

Presign on your server — never ship an API key to a browser — and hand the client just signedUrl and publicUrl:

await fetch(signedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file,
});

The signed URL grants exactly one upload to one key, for one hour, so it is safe to give to a browser. Presign at the moment the user picks a file, not when the page loads, or the URL may expire while they are still filling in a caption.

Accepted files

KindTypes
ImagesJPEG, PNG, WebP, GIF
VideoMP4, QuickTime (.mov), WebM, AVI

Maximum 100 MB per file. Anything else is rejected with unsupported_media_type before you waste bandwidth on it.

MP4 with H.264 video and AAC audio is the safest choice by a wide margin. Every platform accepts it; the others get transcoded, re-encoded, or occasionally refused depending on the codec inside the container.

Attaching media to a post

The media array holds items of { type, url }:

await cutedyno.posts.create({
  content: 'Three shots from the shoot.',
  media: [
    { type: 'image', url: firstUrl },
    { type: 'image', url: secondUrl },
    { type: 'image', url: thirdUrl },
  ],
  accountIds,
});

The post's content type is inferred, not declared. A video in the array makes it a video post; otherwise images make it an image post; otherwise it is text. Do not mix a video and images in one post — the images are ignored, and the result is not what you meant. Send two posts.

Media must be reachable by the platforms, which fetch the URL themselves. URLs returned by /v1/media/upload always are. Your own URLs work too, as long as they are public HTTPS with no signed-URL expiry and no hotlink protection; a URL that 403s for a platform's fetcher produces a publish failure that looks mysterious from your side.

Platform limits worth knowing

These are enforced by the platforms, at publish time, not by CuteDyno at upload time. A file that uploads fine can still fail to publish.

PlatformLimits
InstagramUp to 10 images in a carousel. Video becomes a Reel. Aspect ratio between 4:5 and 1.91:1 for feed images.
FacebookMultiple images supported. Video up to 240 minutes in theory, far less in practice.
TikTokVideo only, plus photo posts. Maximum duration depends on the creator's account, not on your app — CuteDyno reads it per account and rejects longer videos before uploading.
LinkedInMultiple images supported. Video between 3 seconds and 30 minutes.
YouTubeVideo only, and platforms.youtube.title is required. Unverified channels are capped at 15 minutes.

Call POST /v1/posts/validate before publishing to a multi-platform set. It catches structural problems — a video sent to a text-only target, a missing YouTube title — without spending an upload.

For the full per-platform picture, see the platform guides.

Reusing an upload

publicUrl is stable and permanent. Upload a brand asset once, store the URL, and reference it across as many posts as you like. There is no need to re-upload the same file for each post or each account.

Files are stored per profile. Uploading with profileId set puts the file in that customer's namespace, which is what you want on a multi-tenant platform so one customer's assets never appear in another's media library.

Next