Skip to content

Working example

This is the acceptance scenario of the API, the one that must run end to end against the live server: a video from disk becomes a scheduled YouTube post, and the script waits until it is live or failed. The same six calls, in three languages; pick the tab you write in. Response codes and bodies are the ones in openapi.json.

  • An API key with write in the environment as DROPSLATE_API_KEY.
  • A connected YouTube channel (GET /accounts shows its id and status: "ok").
  • A video file, launch.mp4, within YouTube’s limits (Media requirements).
# Call Answer
1 GET /accounts 200 - the account id to target
2 POST /media/uploads {filename, mime, size} 201 - {mediaId, uploadUrl, expiresAt}
3 PUT uploadUrl with the bytes 200 - the URL is signed on its own; no Authorization header
4 GET /media/{id} until status is ready 200 - uploading → processing → ready, or failed with failReason
5 POST /posts/plan, then POST /posts with confirm: true 200 with rows, then 201 with posts[]
6 GET /posts/{id} until status is live or failed 200 - externalUrl, or error
Terminal window
set -e
KEY="$DROPSLATE_API_KEY"; API="https://api.dropslate.top/v1"; AUTH="Authorization: Bearer $KEY"; JSON="Content-Type: application/json"
FILE=./launch.mp4
# 1) The channel to publish to
ACCOUNT_ID=$(curl -s "$API/accounts" -H "$AUTH" | jq -r '.accounts[] | select(.platform=="youtube" and .status=="ok") | .id' | head -1)
# 2) Reserve the upload
TICKET=$(curl -s -X POST "$API/media/uploads" -H "$AUTH" -H "$JSON" \
-d "{\"filename\":\"launch.mp4\",\"mime\":\"video/mp4\",\"size\":$(stat -c%s "$FILE")}")
MEDIA_ID=$(echo "$TICKET" | jq -r .mediaId); UPLOAD_URL=$(echo "$TICKET" | jq -r .uploadUrl)
# 3) Send the bytes (Content-Length must equal the size above)
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: video/mp4" --data-binary @"$FILE" > /dev/null
# 4) Wait for processing
until STATUS=$(curl -s "$API/media/$MEDIA_ID" -H "$AUTH" | jq -r .status); [ "$STATUS" = "ready" ]; do
[ "$STATUS" = "failed" ] && { curl -s "$API/media/$MEDIA_ID" -H "$AUTH" | jq -r .failReason; exit 1; }
sleep 5
done
# 5) Plan, then create
BODY=$(jq -n --arg a "$ACCOUNT_ID" --arg m "$MEDIA_ID" '{
targets:{accountIds:[$a]}, mediaIds:[$m],
title:"Launch day", text:"We are live. Details in the pinned comment.",
when:{publishAt:"2026-09-29T18:00", timezone:"Europe/Kiev"},
perPlatform:{youtube:{visibility:"unlisted", category:"28"}} }')
PLAN=$(curl -s -X POST "$API/posts/plan" -H "$AUTH" -H "$JSON" -d "$BODY")
[ "$(echo "$PLAN" | jq -r .ok)" = "true" ] || { echo "$PLAN" | jq -r .table; exit 1; }
POST_ID=$(echo "$BODY" | jq '.+{confirm:true}' | curl -s -X POST "$API/posts" -H "$AUTH" -H "$JSON" -d @- | jq -r '.posts[0].id')
# 6) Wait for the network's answer
until POST=$(curl -s "$API/posts/$POST_ID" -H "$AUTH"); S=$(echo "$POST" | jq -r .status); [ "$S" = "live" ] || [ "$S" = "failed" ]; do sleep 30; done
echo "$POST" | jq '{status, externalUrl, error, statusLine}'
  • Step 2, 201. uploadUrl is single-use and valid for one hour (expiresAt); mediaId is already the file’s id and does not change. 413-class problems are reported here as 402 quota_exceeded when the plan’s per-file or storage limit is exceeded.
  • Step 3. The PUT needs no Authorization header - the URL is signed - and Content-Length must equal size; a mismatch answers size_mismatch, a second PUT answers already_uploaded.
  • Step 4. A video takes seconds to a minute in processing while its duration and a thumbnail are read. failed names the reason, usually a format the kind does not accept.
  • Step 5, 200 then 201. The plan’s rows[].issues is where caption_too_long, aspect_unsupported or media_not_ready appear; quota.requested is what this request costs against the month. POST /posts answers 409 confirm_required without the flag and 402 quota_exceeded past the limit.
  • Step 6. A post scheduled ahead stays queued until its time; poll every minute at most. live carries externalUrl and, for YouTube, notes in the last event when a thumbnail or playlist was skipped; failed carries error and, if the worker will retry on its own, a retriedByWorker event with the time.
Error Cause Fix
size_mismatch on the PUT Content-Length differs from size in step 2. Send the exact byte count; reserve a new upload if the file changed.
already_uploaded A second PUT to the same URL. The first one succeeded; continue with GET /media/{id}.
media_not_ready in the plan Step 4 was skipped. Poll to ready first.
youtube_title_required in the plan No title for a YouTube target. Add one.
quota_exceeded on POST /media/uploads The file is over the plan’s per-file limit, or storage is full. Free is 500 MB per file and 2 GB; see Plans & pricing.