Skip to content
Reading with an AI assistant?View as MarkdownOpen in ClaudeOpen in ChatGPT

Guides

Uploading media & video covers

Media never transits the API — you upload straight to storage with a presigned URL, then register the object so the pipeline picks it up. This is the exact four-step sequence, plus how a video's cover/thumbnail works and how to narrow transcoding to the platforms you actually need.

Browse docs

6 min read

The flow at a glance#

Four calls, all authenticated with your usual API key or OAuth token. The bytes go straight from your machine to storage — SkedCast's API only ever sees a URL.

  • POST /media/presign — mint an assetId + upload URL.
  • PUT <url> — upload the raw bytes.
  • POST /media — register the upload; this is what starts scanning + transcoding.
  • POST /posts — compose, referencing the assetId in mediaIds.

1. Presign an upload#

clientId is optional — an asset can be agency-wide. Declare the real filename, MIME type, and byte size; they're re-verified server-side (magic bytes, not the declared contentType) once the file lands.

bash
curl -X POST https://api.skedcast.com/v1/media/presign \
  -H "Authorization: Bearer sked_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "<clientId>",
    "filename": "launch-teaser.mp4",
    "contentType": "video/mp4",
    "sizeBytes": 24831992
  }'
Presign a direct upload.
json
{
  "data": {
    "assetId": "b3f1a2c4-5d6e-4f70-8a9b-1c2d3e4f5a6b",
    "key": "acme-agency/…/b3f1a2c4-…/launch-teaser.mp4",
    "url": "https://<presigned-upload-host>/…",
    "expiresAt": "2026-08-06T15:10:00Z"
  },
  "meta": { "correlationId": "c-…" }
}
Response — keep assetId and key for step 3.

2. Upload the bytes#

PUT the file straight to the presigned url from step 1 — no Authorization header, no SkedCast involvement. Match the Content-Type you declared at presign.

bash
curl -X PUT "<url from step 1>" \
  -H "Content-Type: video/mp4" \
  --data-binary @launch-teaser.mp4
Upload directly to storage.

3. Register the asset#

Registering is what starts the pipeline — a virus scan, then a single safe master, then only the per-platform renditions a platform genuinely needs. The response carries readyEvent: "media.ready" — subscribe an endpoint to that event (POST /webhooks) and you are told the instant the asset is publishable. Poll GET /media/:id only if you cannot receive inbound HTTP, and when you do, wait on publishable: true — NOT on a platform appearing in variants[]. A platform the safe master already satisfies (YouTube for a 1080p H.264 MP4, for example) never gets a variants row, so a loop waiting for one waits forever; readiness gives you one verdict per platform (ready / preparing / failed) without that ambiguity.

platforms is optional. Omit it and SkedCast prepares a rendition for every platform you have a live connected account on (scoped to clientId when you send one) — the truthful upper bound on where this asset could actually be published. Pass an explicit list, e.g. ["youtube", "tiktok"], to narrow further and skip renditions you know you won't need. If a post later targets a platform with no rendition yet, the publish pipeline fills the gap on demand and parks that target until it's ready — it doesn't fail.

bash
curl -X POST https://api.skedcast.com/v1/media \
  -H "Authorization: Bearer sked_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "assetId": "b3f1a2c4-5d6e-4f70-8a9b-1c2d3e4f5a6b",
    "key": "acme-agency/…/b3f1a2c4-…/launch-teaser.mp4",
    "clientId": "<clientId>",
    "contentType": "video/mp4",
    "sizeBytes": 24831992,
    "filename": "launch-teaser.mp4",
    "platforms": ["youtube", "tiktok"]
  }'
Register the uploaded object.
json
{
  "data": {
    "assetId": "b3f1a2c4-5d6e-4f70-8a9b-1c2d3e4f5a6b",
    "status": "pending",
    "readyEvent": "media.ready"
  },
  "meta": { "correlationId": "c-…" }
}
Response — the asset is queued, and names the event that says it is done.
bash
curl -X POST https://api.skedcast.com/v1/media/import-url \
  -H "Authorization: Bearer sked_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "clientId": "<clientId>", "url": "https://example.com/launch-teaser.mp4" }'
Import from a URL.

4. Compose, with an optional video cover#

Once the asset is transcoded (or even while it's still processing — the publish pipeline waits), reference its assetId in mediaIds on POST /posts as usual.

For a video, you can also set a custom cover/thumbnail. A cover is a separately uploaded IMAGE asset — run it through the exact same presign → PUT → register sequence above — and pass its assetId as coverMediaId on the post body. YouTube, Instagram, Facebook, and Telegram accept a custom cover image this way; omit coverMediaId to use the auto-extracted poster frame instead.

TikTok doesn't take a cover image — it takes a timestamp. Set coverFrameMs (milliseconds into the video) to pick which frame TikTok uses; it's independent of coverMediaId and applies even when you also set an image cover for the other platforms.

Setting a cover is best-effort by contract: it never fails the publish. Check the outcome afterward via coverIssue on GET /post-targets/:id (see the API reference's Post targets section) — null means it applied cleanly (or there was nothing to apply), a short string explains a rejection, a failed fetch, or an unconfirmed result.

bash
curl -X POST https://api.skedcast.com/v1/posts \
  -H "Authorization: Bearer sked_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "<clientId>",
    "content": "Launch day is here 🚀",
    "mediaIds": ["<videoAssetId>"],
    "coverMediaId": "<coverImageAssetId>",
    "targets": { "mode": "selection", "accountIds": ["<accountId>"] },
    "schedule": { "type": "at", "scheduledAt": "2026-08-10T15:00:00Z" }
  }'
Compose a post with the video plus a custom cover image.
mediauploadvideocoverthumbnail

FAQ

Do I need to wait for transcoding before composing the post?
No. You can compose against a still-processing asset — the publish pipeline waits for it (you'll see target.preparing, then target.published). If you want to wait first, subscribe to media.ready (the register response names it as readyEvent) or poll GET /media/:id until ready: true. Do not compare the status string against "ready" — there is no such status, the terminal value is transcoded, and a loop waiting for "ready" will spin until its own retry cap. Break on failure too: failed and quarantined leave ready false forever, so exit when statusCategory is non-null. And do not wait for a platform to appear in variants[] — a platform the safe master already satisfies never gets a row, and a rendition another platform needs is produced on demand at publish time.
How do I get the URL of the post once it's live?
Read the target: GET /posts/:id or GET /post-targets/:id returns externalId (the platform's own post id) and permalink (its public URL). We capture the URL the platform reports when it publishes, so permalink is populated on every platform that gives us one — including TikTok, Instagram and Threads, whose URLs cannot be rebuilt from the id we store. It is null until the target reaches published; for posts published before 2026-09-03 we fall back to deriving the URL from externalId, which works for YouTube, X, Pinterest, LinkedIn and Facebook feed posts and yields null elsewhere. A few cases have no URL to give at all — a SELF_ONLY TikTok post, a Telegram plain group — and we return null rather than a link that 404s. Subscribing to target.published gets you the externalId the moment it happens, without polling.
What's the difference between coverMediaId and coverFrameMs?
coverMediaId points at a separately uploaded IMAGE asset used as the cover on platforms that accept one (YouTube, Instagram, Facebook, Telegram). coverFrameMs is a millisecond offset into the video itself, sent to TikTok as its native cover-frame timestamp — TikTok has no image-cover mechanism. They're independent and can both be set on the same post.
Does clientId have to be set when uploading?
No — it's optional on presign, register, and import-url. Omit it for an agency-wide asset available to every client.

Ready to broadcast everywhere?

Sign up free — no credit card. You land on the Free plan, and you can start a one-time 7-day Studio trial from your workspace whenever you are ready. Connect your first accounts, import a batch, and watch one post fan out across every platform.