Робочий приклад
Це приймальний сценарій API, той, що має проходити від початку до кінця проти живого сервера: відео з диска стає запланованим постом YouTube, і скрипт чекає, доки він не стане live або failed. Ті самі шість викликів трьома мовами; оберіть вкладку, якою пишете. Коди й тіла відповідей — ті, що в openapi.json.
Перед початком
Section titled “Перед початком”- Ключ API зі
writeу середовищі якDROPSLATE_API_KEY. - Підключений канал YouTube (
GET /accountsпоказує його id іstatus: "ok"). - Відеофайл
launch.mp4у межах лімітів YouTube (Вимоги до медіа).
Шість викликів
Section titled “Шість викликів”| # | Виклик | Відповідь |
|---|---|---|
| 1 | GET /accounts |
200 — id акаунта-цілі |
| 2 | POST /media/uploads {filename, mime, size} |
201 — {mediaId, uploadUrl, expiresAt} |
| 3 | PUT uploadUrl з байтами |
200 — URL підписаний сам по собі; без заголовка Authorization |
| 4 | GET /media/{id}, доки status не ready |
200 — uploading → processing → ready або failed з failReason |
| 5 | POST /posts/plan, потім POST /posts із confirm: true |
200 з рядками, потім 201 з posts[] |
| 6 | GET /posts/{id}, доки status не live або failed |
200 — externalUrl або error |
set -eKEY="$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 toACCOUNT_ID=$(curl -s "$API/accounts" -H "$AUTH" | jq -r '.accounts[] | select(.platform=="youtube" and .status=="ok") | .id' | head -1)
# 2) Reserve the uploadTICKET=$(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 processinguntil 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 5done
# 5) Plan, then createBODY=$(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 answeruntil POST=$(curl -s "$API/posts/$POST_ID" -H "$AUTH"); S=$(echo "$POST" | jq -r .status); [ "$S" = "live" ] || [ "$S" = "failed" ]; do sleep 30; doneecho "$POST" | jq '{status, externalUrl, error, statusLine}'// Node 20+, no dependencies.import { readFile, stat } from 'node:fs/promises';
const API = 'https://api.dropslate.top/v1';const headers = { Authorization: `Bearer ${process.env.DROPSLATE_API_KEY}`, 'Content-Type': 'application/json' };const file = './launch.mp4';const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function api(method, path, body) { const res = await fetch(`${API}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined }); const json = await res.json(); if (!res.ok) throw new Error(`${res.status} ${json.code}: ${json.message} ${json.hint ?? ''}`); return json;}
// 1) The channelconst { accounts } = await api('GET', '/accounts');const account = accounts.find((a) => a.platform === 'youtube' && a.status === 'ok');
// 2) + 3) Reserve the upload and send the bytesconst { size } = await stat(file);const { mediaId, uploadUrl } = await api('POST', '/media/uploads', { filename: 'launch.mp4', mime: 'video/mp4', size });await fetch(uploadUrl, { method: 'PUT', headers: { 'Content-Type': 'video/mp4' }, body: await readFile(file) });
// 4) Wait for processinglet media;do { await sleep(5000); media = await api('GET', `/media/${mediaId}`); if (media.status === 'failed') throw new Error(media.failReason);} while (media.status !== 'ready');
// 5) Plan, then createconst body = { targets: { accountIds: [account.id] }, mediaIds: [mediaId], 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' } },};const plan = await api('POST', '/posts/plan', body);if (!plan.ok) throw new Error(plan.table);const { posts } = await api('POST', '/posts', { ...body, confirm: true });
// 6) Wait for the network's answerlet post;do { await sleep(30000); post = await api('GET', `/posts/${posts[0].id}`);} while (post.status !== 'live' && post.status !== 'failed');console.log(post.statusLine, post.externalUrl ?? post.error);# Python 3.10+, requests.import os, time, requests
API = "https://api.dropslate.top/v1"H = {"Authorization": f"Bearer {os.environ['DROPSLATE_API_KEY']}"}FILE = "./launch.mp4"
def api(method, path, **kw): r = requests.request(method, API + path, headers=H, timeout=30, **kw) if not r.ok: e = r.json() raise RuntimeError(f"{r.status_code} {e['code']}: {e['message']} {e.get('hint', '')}") return r.json()
# 1) The channelaccount = next(a for a in api("GET", "/accounts")["accounts"] if a["platform"] == "youtube" and a["status"] == "ok")
# 2) + 3) Reserve the upload and send the bytessize = os.path.getsize(FILE)ticket = api("POST", "/media/uploads", json={"filename": "launch.mp4", "mime": "video/mp4", "size": size})with open(FILE, "rb") as f: requests.put(ticket["uploadUrl"], data=f, headers={"Content-Type": "video/mp4"}, timeout=600).raise_for_status()
# 4) Wait for processingwhile True: media = api("GET", f"/media/{ticket['mediaId']}") if media["status"] == "ready": break if media["status"] == "failed": raise RuntimeError(media["failReason"]) time.sleep(5)
# 5) Plan, then createbody = { "targets": {"accountIds": [account["id"]]}, "mediaIds": [ticket["mediaId"]], "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 = api("POST", "/posts/plan", json=body)if not plan["ok"]: raise RuntimeError(plan["table"])post_id = api("POST", "/posts", json={**body, "confirm": True})["posts"][0]["id"]
# 6) Wait for the network's answerwhile True: post = api("GET", f"/posts/{post_id}") if post["status"] in ("live", "failed"): break time.sleep(30)print(post["statusLine"], post.get("externalUrl") or post.get("error"))Що означає кожна відповідь
Section titled “Що означає кожна відповідь”- Крок 2,
201.uploadUrlодноразовий і дійсний годину (expiresAt);mediaIdуже є id файлу й не змінюється. Проблеми класу413повідомляються тут як402 quota_exceeded, коли перевищено ліміт тарифу на файл чи сховище. - Крок 3.
PUTне потребує заголовкаAuthorization— URL підписаний — іContent-Lengthмає дорівнюватиsize; невідповідність відповідаєsize_mismatch, другийPUT—already_uploaded. - Крок 4. Відео проводить від секунд до хвилини в
processing, поки читаються тривалість і мініатюра.failedназиває причину, зазвичай формат, якого вид не приймає. - Крок 5,
200, потім201.rows[].issuesплану — там зʼявляютьсяcaption_too_long,aspect_unsupportedабоmedia_not_ready;quota.requested— скільки цей запит коштує проти місяця.POST /postsвідповідає409 confirm_requiredбез прапорця і402 quota_exceededпонад ліміт. - Крок 6. Пост, запланований наперед, лишається
queuedдо свого часу; опитуйте не частіше ніж раз на хвилину.liveнесеexternalUrlі, для YouTube,notesв останній події, коли мініатюру чи плейлист пропущено;failedнесеerrorі, якщо воркер повторить сам, подіюretriedByWorkerз часом.
Типові помилки
Section titled “Типові помилки”| Помилка | Причина | Що робити |
|---|---|---|
size_mismatch на PUT |
Content-Length відрізняється від size у кроці 2. |
Надішліть точну кількість байтів; зарезервуйте нове завантаження, якщо файл змінився. |
already_uploaded |
Другий PUT на той самий URL. |
Перший вдався; продовжуйте з GET /media/{id}. |
media_not_ready у плані |
Крок 4 пропущено. | Спершу опитайте до ready. |
youtube_title_required у плані |
Немає title для цілі YouTube. |
Додайте. |
quota_exceeded на POST /media/uploads |
Файл понад ліміт тарифу на файл, або сховище повне. | Free — 500 МБ на файл і 2 ГБ; див. Тарифи й ліміти. |
Повʼязане
Section titled “Повʼязане”- Швидкий старт — той самий потік без файлу.
- Завантаження, Стани поста
- Довідник OpenAPI