Skip to content

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://<bucket>.r2.cloudflarestorage.com/…",
    "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 transcoding into the renditions each platform needs. The asset is status: "pending" until that finishes; poll GET /media/:id or subscribe to the media.ready webhook event.

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" },
  "meta": { "correlationId": "c-…" }
}
Response — the asset is queued, not ready yet.
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 the platform's rendition to be ready, and if a rendition wasn't prepared at all (e.g. you narrowed platforms and later target a different one), it's produced on demand and the target is parked until it lands.
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.

Be first in line when SkedCast opens

Join the waitlist — agencies on it get early access and launch-day onboarding.