Главная → API документация

API Плейсвида

Скачивание видео из своих программ: три метода, ответы в JSON, ключ в заголовке X-API-Key.

Содержание
  1. Как это работает
  2. Ключ API и тариф Business
  3. GET /api/v1/info
  4. POST /api/v1/download
  5. GET /api/v1/jobs/{job_id}
  6. Лимиты
  7. Ошибки
  8. Примеры: Bash, Python, JavaScript
  9. OpenAPI и поддержка

Как это работает

Базовый адрес: https://placevid.ru/api/v1. Запросы и ответы в JSON (UTF-8). В каждом запросе нужен заголовок X-API-Key с вашим ключом.

  1. GET /api/v1/info?url=…: узнать название, длительность и доступные форматы. Шаг не обязателен.
  2. POST /api/v1/download: заказать скачивание. Номер заказа job_id приходит сразу, файл готовится в фоне.
  3. GET /api/v1/jobs/{job_id}: проверять статус раз в несколько секунд, пока не придёт done.
  4. Скачать файл по ссылке file_url из ответа. Для этой ссылки ключ не нужен.

Успешные ответы устроены одинаково: {"ok": true, …, "rate_remaining": N}. Данные о видео и о заказе лежат в поле data, а rate_remaining показывает, сколько запросов осталось в часовом лимите ключа.

Скачивания по ключу идут по тарифу вашего аккаунта на сайте: бесплатно 1 скачивание в сутки (общее с сайтом), на Pro и Max без суточного лимита. Подробнее в разделе «Лимиты».

Ключ API и тариф Business

Ключ выглядит так: pv_live_ и ещё 40 латинских букв и цифр. Его передают в заголовке каждого запроса:

X-API-Key: pv_live_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b

Как получить ключ

Кнопки для ключей в личном кабинете пока нет. Ключ создаётся запросом из браузера, в котором вы вошли на placevid.ru:

  1. Войдите на placevid.ru/account.
  2. На этой же вкладке откройте консоль браузера (клавиша F12, вкладка «Console») и выполните:
fetch('/api/me/keys', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({name: 'my-bot'})
}).then(r => r.json()).then(console.log)

Ключ придёт в поле key. Он показывается один раз, сохраните его сразу. Название name (до 64 символов) нужно только вам, чтобы различать ключи. Активных ключей у аккаунта может быть до 5.

Список ключей: показывает начало ключа (prefix), число запросов и время последнего использования, сам ключ не показывает.

fetch('/api/me/keys').then(r => r.json()).then(console.log)

Отключить ключ (вместо ID подставьте id из списка). Отключённый ключ обратно не включается, при необходимости создайте новый:

fetch('/api/me/keys/ID', {method: 'DELETE'}).then(r => r.json()).then(console.log)

Тариф Business

Подключается в личном кабинете кнопкой «Подключить Business»: 2 990 ₽ за 30 дней. Business поднимает лимит запросов для всех ключей аккаунта с 600 до 10 000 в час. Суточный лимит скачиваний и качество он не меняет: они зависят от тарифа Free, Pro или Max. Ключи работают и без Business.

Как хранить ключ

GET /api/v1/info: данные о видео

Возвращает название, длительность, превью и список форматов. Файл при этом не скачивается, суточный лимит скачиваний не тратится.

curl -s -G "https://placevid.ru/api/v1/info" \
  -H "X-API-Key: $PV_API_KEY" \
  --data-urlencode "url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"

Параметры

ПараметрТипОбязателенОписание
urlstringдаСсылка на видео, до 2048 символов. Передаётся в строке запроса, не забудьте её закодировать.

Ответ 200 (пример, сокращён):

{
  "ok": true,
  "data": {
    "title": "Rick Astley - Never Gonna Give You Up",
    "thumbnail": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
    "duration": 213,
    "channel": "Rick Astley",
    "video_formats": [
      {"format_id": "137", "label": "1080p", "ext": "mp4", "filesize": 80000000,
       "type": "video", "height": 1080, "total_size": 83400000},
      {"format_id": "136", "label": "720p", "ext": "mp4", "filesize": 40000000,
       "type": "video", "height": 720, "total_size": 43400000}
    ],
    "audio_formats": [
      {"format_id": "140", "label": "129kbps", "ext": "m4a", "filesize": 3400000,
       "type": "audio", "abr": 129.5}
    ],
    "subtitles": [{"lang": "en", "name": "English", "auto": false}],
    "platform": "youtube",
    "view_count": 1700000000,
    "uploader": "Rick Astley",
    "upload_date": "20091025",
    "uploaded_at": "25.10.2009",
    "timestamp": 1256453246
  },
  "rate_remaining": 599
}
Поле в dataЧто это
title, thumbnailНазвание и ссылка на превью.
durationДлительность в секундах. Может быть null.
channel, uploaderКанал и автор, если площадка их отдаёт.
video_formatsФорматы видео от лучшего к худшему. format_id передавайте в /download; label и height: качество (например, 720p); filesize и total_size: примерный размер в байтах, может быть null. Список показывает, что есть у площадки, а скачать можно не выше потолка вашего тарифа.
audio_formatsЗвуковые дорожки площадки (label и abr в kbps). Чтобы получить MP3, передайте в /download "audio_only": true.
subtitlesКакие субтитры есть у ролика: lang, name, auto (созданы автоматически). Только для справки: через API субтитры не скачиваются.
platformПлощадка: youtube, vk, rutube, tiktok и другие.
view_count, upload_date, uploaded_at, timestampПросмотры и дата публикации: upload_date в виде ГГГГММДД, uploaded_at в виде ДД.ММ.ГГГГ, timestamp во времени Unix. Бывают пустыми.

Набор полей зависит от площадки: у некоторых сайтов часть полей пустая. Со временем могут появиться новые поля, незнакомые поля просто пропускайте.

Сервер ждёт ответа площадки до 30 секунд. Если она отвечает дольше, придёт 504 (см. «Ошибки»): повторите запрос позже.

POST /api/v1/download: заказать скачивание

curl -s -X POST "https://placevid.ru/api/v1/download" \
  -H "X-API-Key: $PV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "format_id": "136"}'

Тело запроса (JSON)

ПолеТипОбязательноОписание
urlstringдаСсылка на видео, до 2048 символов.
format_idstringнетformat_id из /info, до 32 символов. Без него качество выберет сервер.
audio_onlybooleanнетtrue: только звук, файл MP3 192 kbps. По умолчанию false.

Другие поля в теле не учитываются.

Ответ 200

{
  "ok": true,
  "job_id": "job_3f9a1c0b7d2e4a6b8c01",
  "status": "preparing",
  "rate_remaining": 598
}

GET /api/v1/jobs/{job_id}: статус заказа

curl -s "https://placevid.ru/api/v1/jobs/job_3f9a1c0b7d2e4a6b8c01" -H "X-API-Key: $PV_API_KEY"

Ответ 200, заказ в работе

{
  "ok": true,
  "data": {
    "status": "downloading",
    "progress": 42,
    "downloaded": 12582912,
    "total": 29884416,
    "error": null,
    "pausable": true,
    "resumable": false,
    "cancellable": true
  },
  "rate_remaining": 590
}

Ответ 200, файл готов

{
  "ok": true,
  "data": {
    "status": "done",
    "progress": 100,
    "filename": "Rick Astley - Never Gonna Give You Up.mp4",
    "display_name": "Rick Astley - Never Gonna Give You Up.mp4",
    "size": 29884416,
    "subtitle_files": [],
    "pausable": false,
    "resumable": false,
    "cancellable": false,
    "file_url": "https://placevid.ru/api/file/0a1b2c3d"
  },
  "rate_remaining": 589
}
statusЧто значитЧто делать
preparingЗаказ принят, сервер начинает скачивание. progress равен 0.Ждать.
downloadingИдёт скачивание. progress от 0 до 100, downloaded и total в байтах.Ждать.
queuedЗаказ ждёт очереди.Ждать.
paused, resumingСкачивание на паузе или продолжается.Ждать.
doneФайл готов, есть file_url.Скачать файл.
errorНе получилось, причина в поле error.Проверить ссылку, заказать заново.
cancelled, expiredЗаказ отменён или его срок истёк.Заказать заново.

Лимиты

Запросы к API

Лимит считается на каждый ключ: 600 запросов в час ко всем методам вместе (/info, /download, /jobs). С тарифом Business: 10 000 запросов в час. Час отсчитывается от первого запроса, после этого счётчик обнуляется. Скачивание файла по file_url и запросы с неверным ключом в лимит не входят.

Остаток виден в поле rate_remaining каждого успешного ответа. Когда лимит исчерпан, сервер отвечает 429 с кодом RATE_LIMIT_EXCEEDED и заголовками Retry-After (через сколько секунд можно продолжить), X-RateLimit-Limit и X-RateLimit-Remaining:

{
  "error": {"code": "RATE_LIMIT_EXCEEDED", "message": "Превышен лимит запросов: 600 в час. Повторите через 12 мин."},
  "detail": "Превышен лимит запросов: 600 в час. Повторите через 12 мин."
}

Скачивания

Скачивания по ключу считаются так же, как на сайте, по тарифу аккаунта, которому принадлежит ключ:

Запросы /info и /jobs суточный лимит скачиваний не тратят. Цены тарифов: /tariffs.

Ошибки

Ошибка приходит с HTTP-кодом и JSON, где есть машинный код и понятный текст. Тот же текст продублирован в поле detail:

{
  "error": {"code": "INVALID_API_KEY", "message": "Ключ не найден или отключён."},
  "detail": "Ключ не найден или отключён."
}

Исключение: при неверных полях запроса (код 422) приходит только detail, и это список, где для каждого поля указано, что с ним не так.

HTTPcodeКогдаЧто делать
400BAD_REQUESTСсылка не открылась, площадка не поддерживается или адрес не разрешён.Проверить ссылку в браузере. Тот же запрос повторять бесполезно.
401INVALID_API_KEYНет заголовка X-API-Key, ключ неверный, отключён, или аккаунт владельца удалён.Проверить ключ, при необходимости создать новый.
402QUOTA_EXCEEDEDБесплатное скачивание на сегодня уже израсходовано (только /download).Подождать сброса счётчика или перейти на Pro или Max.
403FORBIDDENСервер скачивания отказал в доступе к ролику.Проверить, что видео открыто всем.
404JOB_NOT_FOUND, NOT_FOUNDНет заказа с таким номером у вашего аккаунта, или видео не найдено.Проверить номер или ссылку.
413FILE_TOO_LARGEФайл больше потолка тарифа.Выбрать качество ниже или тариф выше.
422нетНеверные параметры: нет url, url длиннее 2048 символов, format_id длиннее 32, тело не JSON.Исправить запрос.
429RATE_LIMIT_EXCEEDEDИсчерпан часовой лимит запросов ключа.Подождать столько секунд, сколько в Retry-After.
429BUSYСервер скачивания сейчас занят.Повторить через минуту.
502UPSTREAM_UNAVAILABLE, UPSTREAM_ERRORСервер скачивания временно недоступен или ответил ошибкой.Повторить позже.
504UPSTREAM_TIMEOUTСервер скачивания не ответил вовремя.Повторить позже.

Ошибки самого скачивания (ролик недоступен, файл слишком большой и т. п.) обычно приходят не кодом ответа, а статусом error в /jobs/{job_id} с текстом в поле error.

Повторять имеет смысл запросы с кодами 429, 502 и 504, с паузой, которая растёт с каждой попыткой. Остальные коды значат, что нужно исправить запрос или сменить тариф. В письме в поддержку укажите время запроса, ссылку на видео, номер заказа и текст ошибки.

Примеры

Все три примера делают одно и то же: заказывают скачивание, ждут готовности и сохраняют файл. Ключ берут из переменной окружения PV_API_KEY.

Bash (нужны curl и jq)

#!/usr/bin/env bash
# Запуск: PV_API_KEY=pv_live_... ./pv.sh "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
set -euo pipefail
API="https://placevid.ru/api/v1"
KEY="${PV_API_KEY:?Задайте переменную PV_API_KEY}"
URL="$1"

# 1. Запускаем задачу
resp=$(curl -sS -X POST "$API/download" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg url "$URL" '{url: $url, audio_only: false}')" \
  -w '\n%{http_code}')
code=$(tail -n1 <<<"$resp")
body=$(sed '$d' <<<"$resp")
if [ "$code" != "200" ]; then
  echo "Ошибка $code: $(jq -r '.error.message // .detail' <<<"$body")" >&2; exit 1
fi
job=$(jq -r '.job_id // empty' <<<"$body")
[ -n "$job" ] || { echo "В ответе нет job_id: $body" >&2; exit 1; }
echo "Задача $job"

# 2. Ждём готовности (до 30 минут, запрос раз в 4 секунды)
status=""
for i in $(seq 1 450); do
  st=$(curl -sS "$API/jobs/$job" -H "X-API-Key: $KEY")
  status=$(jq -r '.data.status // empty' <<<"$st")
  case "$status" in
    done) break ;;
    error|cancelled|expired)
      echo "Задача: $status, $(jq -r '.data.error // ""' <<<"$st")" >&2; exit 1 ;;
    "") echo "Ошибка: $(jq -r '.error.message // .detail // .' <<<"$st")" >&2; exit 1 ;;
  esac
  echo "$status $(jq -r '.data.progress // 0' <<<"$st")%"
  sleep 4
done
[ "$status" = "done" ] || { echo "Не дождались готовности" >&2; exit 1; }

# 3. Скачиваем файл
file_url=$(jq -r '.data.file_url' <<<"$st")
name=$(jq -r '.data.filename // empty' <<<"$st")
name=$(basename "${name:-$job.mp4}")
curl -sS -L -o "$name" "$file_url"
echo "Готово: $name"

Python 3 (библиотека requests)

import os
import time

import requests

API = "https://placevid.ru/api/v1"
HEADERS = {"X-API-Key": os.environ["PV_API_KEY"]}


def api_error(r):
    try:
        body = r.json()
    except ValueError:
        return RuntimeError(f"HTTP {r.status_code}: {r.text[:300]}")
    err = body.get("error") or {}
    return RuntimeError(f"HTTP {r.status_code} {err.get('code', '')}: {err.get('message') or body.get('detail')}")


def get_info(url):
    r = requests.get(f"{API}/info", headers=HEADERS, params={"url": url}, timeout=60)
    if r.status_code != 200:
        raise api_error(r)
    return r.json()["data"]


def start_download(url, format_id=None, audio_only=False):
    body = {"url": url, "audio_only": audio_only}
    if format_id:
        body["format_id"] = format_id
    r = requests.post(f"{API}/download", headers=HEADERS, json=body, timeout=60)
    if r.status_code != 200:
        raise api_error(r)
    job_id = r.json().get("job_id")
    if not job_id:
        raise RuntimeError("В ответе нет job_id, повторите запрос позже")
    return job_id


def wait_job(job_id, every=4, max_wait=1800):
    deadline = time.time() + max_wait
    while time.time() < deadline:
        r = requests.get(f"{API}/jobs/{job_id}", headers=HEADERS, timeout=30)
        if r.status_code == 429:  # лимит запросов: ждём, сколько сказал сервер
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        if r.status_code != 200:
            raise api_error(r)
        job = r.json()["data"]
        status = job.get("status")
        if status == "done":
            return job
        if status in ("error", "cancelled", "expired"):
            raise RuntimeError(f"Задача {job_id}: {status}, {job.get('error')}")
        print(status, job.get("progress", 0), "%")
        time.sleep(every)
    raise TimeoutError(f"Задача {job_id} не готова за {max_wait} с")


def save_file(file_url, path):
    with requests.get(file_url, stream=True, timeout=600) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            for chunk in r.iter_content(1024 * 1024):
                f.write(chunk)
    return path


if __name__ == "__main__":
    link = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
    info = get_info(link)
    print(info["title"], info["duration"], "с")
    # берём лучшее качество не выше 1080p
    fmt = next((f for f in info["video_formats"] if (f.get("height") or 0) <= 1080), None)
    job_id = start_download(link, format_id=fmt["format_id"] if fmt else None)
    job = wait_job(job_id)
    name = os.path.basename(job.get("filename") or f"{job_id}.mp4")
    save_file(job["file_url"], name)
    print("Готово:", name)

JavaScript (Node.js 18 и новее)

// Node.js 18+. Запуск: PV_API_KEY=pv_live_... node pv.mjs "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
import { createWriteStream } from 'node:fs';
import { basename } from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';

const API = 'https://placevid.ru/api/v1';
const KEY = process.env.PV_API_KEY;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function call(path, options = {}) {
  const res = await fetch(API + path, {
    ...options,
    headers: { 'X-API-Key': KEY, 'Content-Type': 'application/json' },
  });
  const body = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${body.error?.message ?? JSON.stringify(body.detail ?? body)}`);
  return body;
}

export async function downloadVideo(url, { formatId = null, audioOnly = false } = {}) {
  const start = await call('/download', {
    method: 'POST',
    body: JSON.stringify({ url, format_id: formatId, audio_only: audioOnly }),
  });
  if (!start.job_id) throw new Error('В ответе нет job_id, повторите запрос позже');

  for (let i = 0; i < 450; i++) {
    const { data } = await call(`/jobs/${start.job_id}`);
    if (data.status === 'done') {
      const file = await fetch(data.file_url);
      if (!file.ok) throw new Error(`Файл: HTTP ${file.status}`);
      const name = basename(data.filename || `${start.job_id}.mp4`);
      await pipeline(Readable.fromWeb(file.body), createWriteStream(name));
      return name;
    }
    if (['error', 'cancelled', 'expired'].includes(data.status)) {
      throw new Error(`Задача: ${data.status}, ${data.error || ''}`);
    }
    console.log(data.status, `${data.progress ?? 0}%`);
    await sleep(4000);
  }
  throw new Error('Не дождались готовности');
}

const link = process.argv[2] || 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';
downloadVideo(link)
  .then((name) => console.log('Готово:', name))
  .catch((e) => { console.error(e.message); process.exit(1); });

Из других языков API вызывается так же: обычные HTTP-запросы с заголовком X-API-Key и JSON в теле.

OpenAPI и поддержка

Описание методов в машинном виде: https://placevid.ru/api/v1/openapi.json. Из него можно собрать клиент через openapi-generator. Готовых библиотек для API пока нет.

Поддержка и вопросы: senorsap@yandex.ru. Новости сервиса: Telegram-канал @placevid.

Нужен ключ?

Войдите на сайт и создайте ключ, как описано в разделе «Ключ API».

Открыть личный кабинет