API Autoposter
v1REST API для автопостинга: публикация постов и клипов, загрузка медиа, статистика и вебхуки для ВКонтакте, Одноклассников, Telegram и YouTube. Все ответы — в JSON.
Базовый URL
https://autoposter.digital/api/v1API доступен на тарифе «Бизнес». Ключ создаётся в кабинете: Настройки → API.
Машинное описание — openapi.json (OpenAPI 3.1, без ключа). Импортируйте в Postman или сгенерируйте клиент, чтобы не переписывать типы вручную. Список путей в спеке сверяется тестами с реальными эндпоинтами, так что она не расходится с кодом.
Быстрый старт: первая публикация
Пять шагов от ключа до вышедшего ролика. Ключ — в Настройках → API (тариф «Бизнес»). Дальше: узнать accountIds, получить presigned-ссылку под файл, залить байты прямо в хранилище, создать пост, проверить результат по площадкам.
Файл заливается PUT-запросом на uploadUrl мимо нашего сервера. Размер обязан совпасть с заявленным size — он входит в подпись, и хранилище отклонит загрузку при расхождении.
KEY="ВАШ_КЛЮЧ"
BASE="https://autoposter.digital/api/v1"
# 1. Каналы — берём id нужного
curl -s "$BASE/accounts" -H "x-api-key: $KEY"
# 2. Слот под файл: presigned PUT (ответ: data.mediaId, data.uploadUrl)
curl -s -X POST "$BASE/media" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"filename":"clip.mp4","mimeType":"video/mp4","size":8482913,
"width":1080,"height":1920,"durationSec":47}'
# 3. Байты — прямо в хранилище
curl -X PUT "URL_ИЗ_ШАГА_2" \
-H "Content-Type: video/mp4" --data-binary @clip.mp4
# 4. Пост (ответ: data.id)
curl -s -X POST "$BASE/posts" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"accountIds":["acc_123"],"text":"Привет из API",
"mediaIds":["med_abc"],"asClip":true,"publishNow":true,
"idempotencyKey":"demo-001"}'
# 5. Что вышло: статус и ссылка по каждой площадке
curl -s "$BASE/posts/post_777" -H "x-api-key: $KEY"import os, requests
BASE = "https://autoposter.digital/api/v1"
KEY = os.environ["AUTOPOSTER_KEY"]
H = {"x-api-key": KEY}
JSON_H = {**H, "Content-Type": "application/json"}
def data(r):
body = r.json()
if not body.get("ok"):
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
return body["data"]
# 1. Каналы
accounts = data(requests.get(f"{BASE}/accounts", headers=H))
account_id = accounts[0]["id"]
# 2. Слот под файл
path = "clip.mp4"
size = os.path.getsize(path)
media = data(requests.post(f"{BASE}/media", headers=JSON_H, json={
"filename": os.path.basename(path), "mimeType": "video/mp4", "size": size,
"width": 1080, "height": 1920, "durationSec": 47,
}))
# 3. Байты — мимо нашего сервера, размер обязан совпасть с size
with open(path, "rb") as f:
up = requests.put(media["uploadUrl"], data=f,
headers={"Content-Type": "video/mp4", "Content-Length": str(size)})
up.raise_for_status()
# 4. Пост
post = data(requests.post(f"{BASE}/posts", headers=JSON_H, json={
"accountIds": [account_id],
"text": "Привет из API",
"mediaIds": [media["mediaId"]],
"asClip": True, # VK: уйдёт в «Клипы»
"publishNow": True,
"idempotencyKey": "demo-001",
}))
# 5. Результат по площадкам
print(data(requests.get(f"{BASE}/posts/{post['id']}", headers=H))["platforms"])import { readFile } from "node:fs/promises";
const BASE = "https://autoposter.digital/api/v1";
const KEY = process.env.AUTOPOSTER_KEY;
const AUTH = { "x-api-key": KEY };
const JSON_H = { ...AUTH, "Content-Type": "application/json" };
async function data(res) {
const body = await res.json();
if (!body.ok) throw new Error(body.error.code + ": " + body.error.message);
return body.data;
}
// 1. Каналы
const accounts = await data(await fetch(BASE + "/accounts", { headers: AUTH }));
// 2. Слот под файл
const bytes = await readFile("clip.mp4");
const media = await data(await fetch(BASE + "/media", {
method: "POST", headers: JSON_H,
body: JSON.stringify({
filename: "clip.mp4", mimeType: "video/mp4", size: bytes.length,
width: 1080, height: 1920, durationSec: 47,
}),
}));
// 3. Байты — прямо в хранилище
const up = await fetch(media.uploadUrl, {
method: "PUT", headers: { "Content-Type": "video/mp4" }, body: bytes,
});
if (!up.ok) throw new Error("Загрузка не удалась: HTTP " + up.status);
// 4. Пост
const post = await data(await fetch(BASE + "/posts", {
method: "POST", headers: JSON_H,
body: JSON.stringify({
accountIds: [accounts[0].id],
text: "Привет из API",
mediaIds: [media.mediaId],
asClip: true, // VK: уйдёт в «Клипы»
publishNow: true,
idempotencyKey: "demo-001",
}),
}));
// 5. Результат по площадкам
const full = await data(await fetch(BASE + "/posts/" + post.id, { headers: AUTH }));
console.log(full.platforms);Публикация асинхронная: POST /posts возвращает status: "QUEUED", а не результат. Опрашивать GET /posts/{id} в цикле не обязательно — подпишитесь на POST_PUBLISHED и POST_FAILED (см. Вебхуки).
Публикуете в TikTok — задайте tiktok.privacyLevel. Без него ролик уходит «только мне»: см. Параметры площадок.
Авторизация
Каждый запрос передаёт API-ключ воркспейса в заголовке. Поддерживаются два варианта — используйте любой:
x-api-key: ВАШ_КЛЮЧ
# либо
Authorization: Bearer ВАШ_КЛЮЧБез ключа или с неверным — 401 UNAUTHORIZED. Если тариф не «Бизнес» — 403 PLAN_LIMIT. Ключ даёт доступ ко всему воркспейсу — храните его как пароль и не публикуйте в клиентском коде.
Формат ответов
Единая обёртка. Успех — ok: true и data; ошибка — ok: false и error с машинным code.
{ "ok": true, "data": { ... } }{ "ok": false, "error": { "code": "VALIDATION_ERROR", "message": "…", "details": [] } }200 / 201Успех. 201 отдаёт только POST /webhooks (создана подписка). Остальные эндпоинты, включая POST /media и POST /posts, отвечают 200.
400 VALIDATION_ERRORНеверные параметры (см. details).
401 UNAUTHORIZEDНет ключа или он неверный.
402 NO_BALANCEНедостаточно AI-баланса (при autogen).
403 PLAN_LIMITAPI доступен только на тарифе «Бизнес».
404 NOT_FOUNDРесурс не найден или чужой.
429 RATE_LIMITEDПревышен лимит запросов. Секунды до следующей попытки — в заголовке Retry-After и в тексте message.
Полный перечень машинных кодов — в разделе Коды ошибок. Разбирайте error.code, а не текст message: тексты меняются, коды — нет.
Лимиты
Лимит считается на воркспейс, в скользящем окне в одну минуту, отдельным счётчиком на каждую строку таблицы: медиа и посты друг друга не расходуют.
POST /posts30 / минСоздание поста.
POST /media60 / минВыдача presigned-ссылки. Сама заливка байтов идёт в хранилище напрямую и под лимит не попадает.
POST /posts/bulk10 / минЛимит жёстче: за один запрос проходит до 100 операций.
PATCH / DELETE /posts/{id}30 / минПеренос и отмена считаются раздельно — по 30 на каждый метод.
POST /webhooks20 / минРегистрация подписки. GET и DELETE /webhooks — без лимита.
GET /analytics30 / минЕдинственный GET с лимитом.
Остальные GETбез лимита/accounts, /posts, /posts/{id}, /stats, /stats/{postId}. Лимита нет сейчас — не стройте на этом опрос в тугом цикле, ставьте вебхуки.
При превышении — 429, код RATE_LIMITED. Секунды до следующей попытки приходят в заголовке Retry-After и подставлены в текст message. Отдельного поля retryAfter в теле ответа нет — читайте заголовок.
Для массовых действий используйте POST /posts/bulk (до 100 постов за раз), а не цикл одиночных запросов: 100 отмен по одной упрутся в лимит на 31-й.
Коды ошибок
Все коды, которые отдаёт API v1. Машинный код — в error.code, человеческое пояснение — в error.message. У VALIDATION_ERROR дополнительно приходит error.details — список замечаний zod по полям.
UNAUTHORIZED401Нет заголовка с ключом или ключ не найден. Проверьте x-api-key / Authorization: Bearer.
PLAN_LIMIT403Ключ верный, но тариф воркспейса не «Бизнес». Повторять бесполезно — нужно сменить тариф.
RATE_LIMITED429Превышен лимит запросов в минуту. Подождите Retry-After секунд и повторите.
VALIDATION_ERROR400Тело или query не прошли схему (в том числе кривые from/to). Смотрите details, запрос без правки повторять бессмысленно.
INVALID_JSON400Тело не разобралось как JSON: пустое body или нет Content-Type: application/json.
NOT_FOUND404Пост, вебхук или аккаунты не найдены — либо принадлежат другому воркспейсу.
SERVER_ERROR500Ошибка на нашей стороне. Повторите с задержкой; если повторяется — напишите нам.
MEDIA_NOT_FOUND404Часть mediaIds не существует или чужая. Загрузите файлы через POST /media и передайте выданные mediaId.
MEDIA_INGEST400Не удалось скачать файл из mediaUrls: недоступен, неподдерживаемый тип, больше 500 МБ либо больше 100 МБ без Content-Length. Крупные файлы — через presign.
TOO_MANY_MEDIA400Суммарно mediaIds + mediaUrls больше 10 файлов.
NO_VIDEO400В accountIds есть площадка только для видео (YouTube, TikTok), а видео в посте нет.
EMPTY_POST400Ни текста, ни медиа: добавьте text/platformText и/или mediaIds/mediaUrls либо включите autogen.
PAST_DATE400scheduledAt в прошлом. Укажите будущее время или publishNow: true.
AUTOGEN_NO_TOPIC400Включён autogen, но нечего брать за тему: задайте autogen.topic либо text/title.
NO_BALANCE402Кончился AI-баланс. Пополните баланс либо добавьте свой ключ провайдера в настройках.
MODEL_UNAVAILABLE503Запрошенная в autogen.model модель недоступна: нет ключа её провайдера. Выберите другую модель.
NO_KEY503AI не настроен вовсе — ни платформенного ключа, ни своего.
AI_ERROR503Провайдер не ответил или вернул ошибку. Разумно повторить.
AI_PARSE502Модель вернула не JSON — разобрать ответ не удалось. Повторите запрос.
INVALID_FILE_TYPE400mimeType не в списке разрешённых.
FILE_TOO_LARGE400Превышен лимит: 500 МБ для видео, 20 МБ для изображений.
NOT_SUPPORTED400Хранилище инстанса не умеет presigned-загрузку. Отдавайте файл через mediaUrls в POST /posts.
ALREADY_PUBLISHING409Публикация уже началась — отменить нельзя. Список площадок, где не успели, — в тексте сообщения.
NOT_RESCHEDULABLE409Перенести можно только пост в статусе DRAFT, SCHEDULED или QUEUED.
PAST_DATE400Новое scheduledAt в прошлом.
Ошибки публикации — это другое. Всё выше — отказы на приёме запроса. Публикация происходит позже и асинхронно, и её отказ приходит событием POST_FAILED: там errorCode — машинный код площадки (VK_CLIP_REJECTED, 456, INVALID_TOKEN …), error — её дословный ответ, а summary и action — то же самое человеческим языком. Ветвиться следует по errorCode или по fix (reconnect, permissions, wait, content, support, none), а не по тексту. Те же поля есть в GET /posts/{id} в объекте error у площадки.
Видео: лимиты и поведение
Главный принцип: Autoposter никогда не обрезает и не перекодирует видео. Файл уходит на площадку в исходном виде, байт в байт. Если площадка ролик не принимает (длительность, размер, кодек) — вы получаете ошибку публикации с её текстом, а не молча укороченную версию. Кабинет и внешний API используют один и тот же конвейер публикации — лимиты и поведение идентичны.
Видео-файлдо 500 МБЗагрузка через presign (POST /media). Формат — как есть: MP4/H.264/AAC подходят всем площадкам.
Изображениедо 20 МБФайлов на постдо 10Видео к одному посту — одно (ограничение площадок).
mediaUrls (по ссылке)до 100 МБСкачивание файла по публичному URL в POST /posts. Крупнее — только presign.
Длительность / FPS / кодекине проверяемОграничений со стороны Autoposter нет — действуют только правила самих площадок.
YouTubeПолная загрузка через официальный resumable upload, без обрезки. Вертикальный ролик (высота > ширины) длительностью до 3 минут — кандидат в Shorts: мы автоматически добавляем #Shorts, классификацию выполняет сам YouTube (по его правилам Shorts — до 3 минут). Ролики 61–180 секунд поддерживаются одинаково из кабинета и через API.
ВКонтактеС asClip: true ролик пробует уйти через shortVideo.create. Сейчас ВК не даёт приложению Autoposter доступ к этому методу: запрос отклоняется кодом «Unknown method», после чего ролик публикуется обычным видео без изменений файла. ВК может дополнительно классифицировать вертикальное видео как Клип, но Autoposter этого не гарантирует. Собственного лимита «60 секунд» у Autoposter нет.
ОдноклассникиЗагрузка через официальный video.getUploadUrl, файл не изменяется. Максимальная длительность клипов — по правилам ОК.
TikTokЗагрузка целиком (PULL_FROM_URL). Лимиты длительности и размера — правила TikTok для вашего аккаунта.
Telegramдо 50 МБОграничение Bot API самого Telegram; файлы крупнее отклоняются с ошибкой.
Актуальные лимиты длительности самих площадок меняются на их стороне — сверяйтесь с их официальной справкой. Со стороны Autoposter гарантия одна и простая: файл доставляется без изменений, либо вы получаете честную ошибку.
vk.clipOnlyboolean, POST /posts«Клип или ничего»: пост завершается ошибкой VK_CLIP_REJECTED, обычным видео не публикуется. На сегодня ВК не даёт нашему приложению доступ к методу Клипов, поэтому с true пост будет падать ВСЕГДА — не подключайте этот режим в бою. По умолчанию false: срабатывает фолбэк в раздел «Видео» (файл не меняется), с оговоркой в note.
mediaKind"clip" | "video" | nullФактический тип опубликованного видео у ВК. Возвращается в GET /posts/{id} (platforms[].mediaKind) и в вебхуке POST_PUBLISHED.
notestring | nullОговорка площадки при частичном успехе (сработал фолбэк, потерялось фото). Там же: GET /posts/{id} и вебхук.
Фолбэк выполняется только на однозначный отказ VK API. Если запрос клип-метода оборвался по сети или таймауту, фолбэк НЕ срабатывает — попытка уходит в обычный повтор задачи, чтобы исключить одновременное создание Клипа и обычного видео.
Аккаунты
/api/v1/accountsid нужны как accountIds при создании поста.curl https://autoposter.digital/api/v1/accounts \
-H "x-api-key: ВАШ_КЛЮЧ"{ "ok": true, "data": [
{ "id": "acc_123", "platform": "VK", "platformLabel": "ВКонтакте",
"displayName": "Мой паблик", "isActive": true,
"lastVerifiedAt": "2026-08-30T04:12:07.001Z",
"connectedAt": "2026-06-01T10:00:00.000Z", "followers": 2545 }
] }statusactive | inactive | allПо умолчанию active — как было раньше. Отвалившийся аккаунт из списка просто исчезал, неотличимо от удалённого: посты не выходят, а объяснить нечем. Запросите inactive или all, чтобы увидеть такие каналы и показать клиенту причину.
limitnumber1–200, по умолчанию 100.
cursorstringid последнего канала предыдущей страницы.
Страницы отдаются заголовками, а не полями: X-Has-More и X-Next-Cursor. Сам ответ остаётся массивом — клиенты уже написаны под него, и менять форму ради страниц значило бы сломать работающий код.
У отключённого канала isActive: false и поле action с подсказкой. Восстановить доступ через API нельзя: для этого нужен вход в саму соцсеть, поэтому переподключение делает владелец в кабинете.
Загрузка медиа
/api/v1/mediaPUT-запросом загрузить сам файл, (3) передать mediaId в пост. Альтернатива — передать mediaUrls прямо в пост (мы скачаем сами).filenamestringобязателенИмя файла с расширением.
mimeTypestringобязателенНапример image/jpeg, video/mp4.
sizenumberобязателенРазмер в байтах (должен совпасть при заливке).
widthnumberШирина кадра. По ней определяется вертикальность ролика.
heightnumberВысота кадра. Если не передать — прочитаем из файла сами.
durationSecnumberДлительность ролика в секундах.
Для видео стоит передавать width и height: от вертикальности зависит зеркалирование в клип-сети. Если их нет, мы прочитаем размеры из самого файла — это работает и с роликами, где оглавление MP4 лежит в конце (обычный вывод ffmpeg без +faststart).
curl -X POST https://autoposter.digital/api/v1/media \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{ "filename": "clip.mp4", "mimeType": "video/mp4", "size": 8482913,
"width": 1080, "height": 1920, "durationSec": 47 }'{ "ok": true, "data": {
"mediaId": "med_abc", "uploadUrl": "https://…", "method": "PUT",
"expiresInSec": 3600
} }curl -X PUT "URL_ИЗ_ОТВЕТА" \
-H "Content-Type: video/mp4" --data-binary @clip.mp4/api/v1/media/{id}PUT иначе обнаружится только при публикации — ошибкой.{ "ok": true, "data": {
"id": "med_abc", "filename": "clip.mp4", "mimeType": "video/mp4",
"size": 18452301, "width": 1080, "height": 1920, "durationSec": 47,
"uploaded": true, "postId": null, "createdAt": "…"
} }uploaded — ответ самого хранилища, а не наша запись (она создаётся до заливки и сама по себе ничего не доказывает). postId: null означает, что файл ещё не привязан к посту.
/api/v1/media/{id}curl -X DELETE https://autoposter.digital/api/v1/media/med_abc -H "x-api-key: ВАШ_КЛЮЧ"Файл, уже прикреплённый к посту, не удаляется — придёт 409 MEDIA_IN_USE с идентификатором поста: удаление вырвало бы вложение из запланированной или вышедшей публикации. Сначала пост.
Создание поста
/api/v1/postspublishNow: true, по умолчанию) или ставит в расписание (scheduledAt). Текст можно задать общий (text) или свой на площадку (platformText). Медиа — через mediaIds или mediaUrls. Можно включить AI-генерацию текста (autogen).accountIdsstring[]обязателенКуда публиковать (1–50). Из GET /accounts.
textstringТекст поста (до 10000). Либо platformText.
platformTextobjectТекст под площадку: { "VK": "…", "TELEGRAM": "…" }.
titlestringЗаголовок (для видео/YouTube).
hashtagsstring[]До 30 тегов.
mediaIdsstring[]ID из POST /media (до 10).
mediaUrlsstring[]Прямые ссылки на медиа — скачаем сами (до 10).
asClipbooleanВлияет ТОЛЬКО на ВКонтакте: с флагом пробует shortVideo.create, а при недоступности метода откатывается на обычное видео; без него сразу публикует обычное видео. Одноклассники, YouTube и TikTok флаг игнорируют — там формат определяется размерами ролика. По умолчанию false.
publishNowbooleanОпубликовать сразу. По умолчанию true.
scheduledAtstring (ISO)Время публикации в будущем (если не publishNow).
idempotencyKeystringЗащита от дублей: повтор с тем же ключом вернёт тот же пост (окно 10 минут).
sandboxbooleanТестовый режим: пост проходит весь путь — очередь, статусы, вебхуки, — но в соцсеть НЕ отправляется. Для отладки интеграции без публикаций в живые сообщества. По умолчанию false.
autogenobjectAI-генерация: { topic, title, caption, hashtags, model }. Тратит AI-баланс.
tiktokobjectНастройки TikTok. Без privacyLevel ролик выходит «только мне» — см. раздел «Параметры площадок».
vkobjectНастройки ВКонтакте: { clipOnly }. См. раздел «Параметры площадок».
curl -X POST https://autoposter.digital/api/v1/posts \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{
"accountIds": ["acc_123"],
"text": "Привет из API!",
"mediaIds": ["med_abc"],
"asClip": true,
"publishNow": true,
"idempotencyKey": "my-unique-key-001"
}'{ "ok": true, "data": {
"id": "post_777", "status": "QUEUED", "scheduledAt": null,
"mediaCount": 1,
"accounts": [{ "id": "acc_123", "platform": "VK", "displayName": "Мой паблик" }]
} }Параметры площадок в POST /posts
Два необязательных объекта в теле POST /posts: tiktok и vk. Они действуют только на свою площадку, остальные их не видят.
TikTok — объект tiktok
SELF_ONLY.tiktok.mode"direct" | "draft"Куда отправить ролик. direct (по умолчанию) — сразу в профиль с настройками ниже. draft — в черновики TikTok: подпись, приватность, музыку и эффекты автор задаёт сам в приложении, поля privacyLevel и disable* при этом не передаются площадке.
tiktok.privacyLevelstringPUBLIC_TO_EVERYONE | MUTUAL_FOLLOW_FRIENDS | FOLLOWER_OF_CREATOR | SELF_ONLY. Если не передан — SELF_ONLY. Если переданное значение недоступно аккаунту (TikTok не вернул его в списке доступных) — тоже берётся самый закрытый доступный, а не ваш вариант.
tiktok.disableCommentbooleanВыключить комментарии. По умолчанию false. Запрет на стороне аккаунта сильнее: если TikTok сообщает, что комментарии у автора выключены, они останутся выключенными вне зависимости от флага.
tiktok.disableDuetbooleanВыключить дуэты. По умолчанию false, поведение как у disableComment.
tiktok.disableStitchbooleanВыключить склейки. По умолчанию false, поведение как у disableComment.
tiktok.commercialbooleanЗаявление о коммерческом контенте. По умолчанию false. Это переключатель-предохранитель: без него два поля ниже не действуют вообще.
tiktok.brandOrganicbooleanСобственный бренд автора. Уходит в TikTok только вместе с commercial: true.
tiktok.brandedContentbooleanРеклама третьего лица (branded content). Уходит в TikTok только вместе с commercial: true.
curl -X POST https://autoposter.digital/api/v1/posts \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{
"accountIds": ["acc_tt"],
"text": "Подпись ролика",
"mediaIds": ["med_abc"],
"publishNow": true,
"tiktok": {
"mode": "direct",
"privacyLevel": "PUBLIC_TO_EVERYONE",
"commercial": false
}
}'Если у приложения или у токена нет права прямой публикации, mode: "direct" не падает ошибкой: ролик уходит черновиком, а в результат добавляется оговорка note с объяснением. Проверить фактический исход — GET /posts/{id} или событие POST_PUBLISHED.
ВКонтакте — объект vk
vk.clipOnlyboolean«Клип или ничего». По умолчанию false: ролик публикуется обычным видео без изменений файла, а в результат кладётся оговорка note. С true такой пост завершается ошибкой публикации VK_CLIP_REJECTED и обычным видео не выходит. На сегодня ВК не даёт нашему приложению доступ к методу Клипов, поэтому с true пост будет падать ВСЕГДА — режим существует на будущее, для боевой публикации сейчас не годится.
curl -X POST https://autoposter.digital/api/v1/posts \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{
"accountIds": ["acc_vk"],
"text": "Вертикальный ролик",
"mediaIds": ["med_abc"],
"asClip": true,
"vk": { "clipOnly": true }
}'Фолбэк и clipOnly срабатывают только на однозначный отказ VK API. Обрыв сети или таймаут фолбэк не запускает — задача уходит в обычный повтор, чтобы не создать Клип и обычное видео одновременно. Подробности — в разделе Видео: лимиты и поведение.
Список постов
/api/v1/postsstatusstringЧерез запятую: SCHEDULED,PUBLISHED,FAILED и т.д.
fromstringС какой даты. ГГГГ-ММ-ДД или ISO 8601.
tostringПо какую дату включительно (весь день).
limitnumber1–100, по умолчанию 20.
cursorstringid последнего поста предыдущей страницы.
from и to сравниваются с датой, когда пост появляется на ленте времени: у опубликованного — когда вышел, у запланированного — когда выйдет, у черновика без времени — когда создан. Поэтому «все будущие публикации» — это?status=SCHEDULED&from=сегодня, а не выгрузка всей истории.
curl "https://autoposter.digital/api/v1/posts?status=SCHEDULED&from=2026-08-06&limit=100" -H "x-api-key: ВАШ_КЛЮЧ"{ "ok": true, "data": {
"posts": [
{ "id": "post_777", "title": "Анонс", "status": "SCHEDULED",
"scheduledAt": "2026-08-07T09:00:00.000Z", "mediaCount": 1,
"platforms": [
{ "platform": "VK", "platformLabel": "ВКонтакте",
"account": "Афиша города", "accountId": "acc_123",
"status": "QUEUED", "url": null, "publishedAt": null }
] }
],
"nextCursor": null
} }Пост: получить / перенести / удалить
/api/v1/posts/{id}curl https://autoposter.digital/api/v1/posts/post_777 -H "x-api-key: ВАШ_КЛЮЧ"/api/v1/posts/{id}scheduledAtstring (ISO)Новое время публикации, только будущее. Задачи снимаются и планируются заново.
textstringОбщий текст для всех площадок поста.
platformTextobjectТекст под конкретные площадки: { "TIKTOK": "…" }. Перекрывает общий text у этих площадок.
titlestringЗаголовок (используется YouTube и в списке постов).
hashtagsstring[]Заменяет прежний набор хэштегов.
linkstringСсылка, добавляемая к посту.
curl -X PATCH https://autoposter.digital/api/v1/posts/post_777 \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{ "text": "Новый текст", "scheduledAt": "2026-09-01T10:00:00Z" }'Менять можно только то, что ещё не ушло в работу: статусы DRAFT, SCHEDULED, QUEUED. У публикующегося поста правка не успела бы примениться, у вышедшего — бессмысленна; в обоих случаях придёт 409 NOT_EDITABLE. Состав площадок PATCH не меняет: это уже другой пост по смыслу — создайте новый. Перепланировать задачи ради правки текста не нужно: воркер читает текст из базы в момент публикации.
/api/v1/posts/{id}curl -X DELETE https://autoposter.digital/api/v1/posts/post_777 -H "x-api-key: ВАШ_КЛЮЧ"Повтор упавшей публикации
/api/v1/posts/{id}/retryvariantId — одна конкретная.curl -X POST https://autoposter.digital/api/v1/posts/post_778/retry \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{}'{ "ok": true, "data": {
"retrying": 1,
"platforms": [
{ "variantId": "var_991", "platform": "VK", "platformLabel": "ВКонтакте", "jobId": "job_5512" }
],
"skipped": []
} }Отключённые аккаунты в повтор не идут — публикация упала бы тем же способом. Такие площадки возвращаются в skipped с причиной; если повторять нечего вовсе, приходит 409 ACCOUNT_INACTIVE. Если у поста нет упавших площадок — 409 NOT_FAILED. Лимит — 30 запросов в минуту.
Массовые операции
/api/v1/posts/bulkaction"cancel" | "reschedule"обязателенЧто сделать.
postIdsstring[]обязателенID постов (1–100).
scheduledAtstring (ISO)Обязателен для reschedule (будущее время).
curl -X POST https://autoposter.digital/api/v1/posts/bulk \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{ "action": "cancel", "postIds": ["post_1", "post_2"] }'Статистика
/api/v1/statscurl "https://autoposter.digital/api/v1/stats?limit=50" -H "x-api-key: ВАШ_КЛЮЧ"limitnumber1–200, по умолчанию 50.
cursorstringid последнего канала предыдущей страницы. В ответе — поле nextCursor; null означает, что страница последняя.
/api/v1/stats/{postId}{ "ok": true, "data": {
"postId": "post_777", "status": "PUBLISHED",
"totals": { "likes": 42, "comments": 3, "reposts": 1, "views": 1980, "reach": 3120 },
"platforms": [
{ "platform": "VK", "url": "https://vk.com/wall-…",
"stats": { "likes": 42, "views": 1980, "reach": 3120, "fetchedAt": "…" } }
]
} }Аналитика роликов
/api/v1/analyticsPUBLISHED.postIdsstringid постов через запятую — точечный режим, когда у вас уже есть свои id. При нём limit игнорируется, потолок выдачи — 200 вариантов.
platformstringОдна площадка: VK, YOUTUBE, TIKTOK и т.д. Без параметра — все.
fromstringС какой даты. ГГГГ-ММ-ДД или ISO 8601. Окно считается по времени ВЫХОДА публикации, а не создания.
tostringПо какую дату включительно (весь день).
limitnumber1–200, по умолчанию 50. Сортировка — по publishedAt, свежие сверху.
curve1 | trueДобавить кривую удержания. По умолчанию выключена: это до сотни чисел на ролик, и в ответе на полсотни роликов она весит больше всего остального.
views / reachnumberПросмотры и охват в терминах площадки.
likes / comments / reposts / sharesnumberРеакции. Что именно площадка считает репостом, а что «шером», зависит от неё.
watchTimeMinnumber | nullСуммарное время просмотра ролика в МИНУТАХ по всем зрителям. Отдают не все площадки — где не отдают, null.
avgViewSecnumber | nullСредняя длительность одного просмотра в секундах.
retentionPctnumber | nullСредний процент досмотра. Значения больше 100 — норма для зациклённых Shorts: повторный просмотр засчитывается. Мы не обрезаем.
subsGained / subsLostnumber | nullПодписки, полученные и потерянные благодаря этому ролику (по атрибуции площадки).
datestringСутки, к которым относится снимок.
fetchedAtstringКогда сборщик забрал цифры. Возраст данных — по нему.
stats: null означает «ролик вышел, но сборщик до него ещё не дошёл» — это не то же самое, что ноль просмотров, поэтому нулями мы это не подменяем. Сборщик ходит по площадкам примерно раз в два часа: у только что опубликованного поста цифр не будет.
curl "https://autoposter.digital/api/v1/analytics?postIds=post_777,post_778&curve=1" \
-H "x-api-key: ВАШ_КЛЮЧ"{ "ok": true, "data": [
{
"postId": "post_777",
"title": "Анонс",
"platform": "YOUTUBE",
"account": { "id": "acc_yt", "name": "Мой канал" },
"publishedAt": "2026-08-20T09:00:00.000Z",
"remotePostId": "dQw4w9WgXcQ",
"remoteUrl": "https://youtube.com/shorts/dQw4w9WgXcQ",
"stats": {
"views": 18240, "likes": 512, "comments": 33, "reposts": 0, "reach": 18240,
"watchTimeMin": 4310, "avgViewSec": 14, "retentionPct": 82,
"subsGained": 41, "subsLost": 3, "shares": 96,
"date": "2026-08-27T00:00:00.000Z",
"fetchedAt": "2026-08-27T18:04:11.000Z"
},
"retention": {
"curve": [100, 98, 95, 91, 88],
"step": 0.01,
"fetchedAt": "2026-08-27T18:04:11.000Z"
}
}
] }data здесь — массив, а не объект: одна строка на каждую площадку каждого поста. Пост, вышедший в трёх местах, даст три строки с одним postId.
retention.curve — доли досмотра по оси «часть ролика», а не по секундам: step задаёт шаг оси в долях (0.01 — сотые доли ролика). Ролики разной длины так сравнимы напрямую, а в секунды ось переводится умножением на длительность. retention.fetchedAt: null отвечает на вопрос «пусто — это ещё не ходили или площадка не отдала»: null означает, что за кривой ещё не ходили.
Вебхуки
/api/v1/webhooksPOST на ваш URL. GET /webhooks вернёт ваши подписки и полный список событий.POST_CREATED1 на постПост создан — и из кабинета, и через POST /api/v1/posts. Это ещё не публикация: пост может быть запланирован или упасть позже. Поля: postId, title, status.
POST_PUBLISHED1 на площадкуПлощадка приняла публикацию. Пост в три канала даёт три события. Поля: postId, title, platform, account, remoteUrl, mediaKind, note.
POST_FAILED1 на площадкуПубликация окончательно не удалась — после того, как исчерпаны все повторы. Поля: postId, title, platform, account, error (текст причины).
ACCOUNT_DISCONNECTEDКанал перестал отвечать, нужно переподключить. Поля: platform, account, error.
PAYMENT_SUCCESSОплата прошла. Поля: paymentId, planKey, amountRub, userId.
PAYMENT_REFUNDEDВозврат средств. Поля: paymentId, planKey, refundedRub, isFull, userId.
События POST_PUBLISHED и POST_FAILED приходят по одному на каждую площадку поста, а не по одному на пост. Считать пост «полностью вышедшим» по первому событию нельзя — либо сложите площадки сами, либо сверьтесь с GET /posts/{id}.
curl -X POST https://autoposter.digital/api/v1/webhooks \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{ "url": "https://ваш-сервер/hook", "events": ["POST_PUBLISHED", "POST_FAILED"] }'curl -X DELETE https://autoposter.digital/api/v1/webhooks \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{ "id": "wh_123" }'Проверка подписи вебхука
Ваш URL публичный — значит, постучаться по нему может кто угодно. Подпись отвечает на единственный вопрос: пришло ли тело именно от нас. Секрет знают только две стороны, поэтому совпадение HMAC доказывает и авторство, и то, что тело не поменяли в пути.
Content-Typeapplication/jsonТело всегда JSON в UTF-8.
X-Autoposter-EventstringИмя события: POST_PUBLISHED, POST_FAILED и т.д. Дублирует поле event в теле — удобно для маршрутизации до разбора JSON.
X-Autoposter-TimestampISO-8601Момент отправки. Входит в подписываемую строку и защищает от повтора перехваченного запроса: отвергайте всё, что старше вашего окна (разумно 5 минут).
X-Autoposter-SignaturehexHMAC-SHA256 на секрете вебхука от строки «timestamp + точка + сырое тело», в нижнем регистре, без префикса вроде sha256=.
X-Autoposter-DeliverystringИдентификатор доставки. При повторе он ТОТ ЖЕ — по нему отбрасывайте дубли, если ваш ответ потерялся уже после обработки.
Секрет генерируется при регистрации подписки и возвращается в ответе POST /api/v1/webhooks — один раз: GET /webhooks его уже не отдаёт. Потеряли — удалите подписку и создайте заново.
Подписывается строка timestamp + "." + тело, где тело — сырые байты ровно в том виде, в каком пришли. Разобрать JSON и сериализовать обратно нельзя: порядок ключей и пробелы изменятся, и подпись не сойдётся. Сравнивайте строки в постоянном времени (timingSafeEqual, hmac.compare_digest).
{
"event": "POST_PUBLISHED",
"data": { … },
"timestamp": "2026-08-28T09:00:03.412Z"
}import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.AUTOPOSTER_HOOK_SECRET;
// express.raw — обязательно: подпись считается по сырым байтам,
// а express.json() их уже не сохраняет.
app.post("/hook", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-Autoposter-Timestamp") ?? "";
// Не принимаем старьё: защита от повтора перехваченного запроса.
if (Math.abs(Date.now() - Date.parse(ts)) > 5 * 60_000) return res.sendStatus(401);
const signed = Buffer.concat([Buffer.from(ts + ".", "utf8"), req.body]);
const expected = crypto.createHmac("sha256", SECRET).update(signed).digest("hex");
const got = req.get("X-Autoposter-Signature") ?? "";
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(got, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body.toString("utf8"));
console.log(payload.event, payload.data);
// Отвечайте быстро: мы ждём ответ не дольше 10 секунд.
res.sendStatus(200);
});
app.listen(3000);import hmac, hashlib, os
from datetime import datetime, timezone
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["AUTOPOSTER_HOOK_SECRET"].encode()
@app.post("/hook")
def hook():
ts = request.headers.get("X-Autoposter-Timestamp", "")
# Не принимаем старьё: защита от повтора перехваченного запроса.
sent = datetime.fromisoformat(ts.replace("Z", "+00:00"))
if abs((datetime.now(timezone.utc) - sent).total_seconds()) > 300:
abort(401)
# request.get_data() — сырые байты тела, до разбора JSON.
signed = ts.encode() + b"." + request.get_data()
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
got = request.headers.get("X-Autoposter-Signature", "")
if not hmac.compare_digest(expected, got):
abort(401)
payload = request.get_json()
print(payload["event"], payload["data"])
return "", 200Доставка с повторами: таймаут ответа — 10 секунд, успехом считается любой код 2xx. Если ответ не пришёл или пришёл не 2xx, событие повторяется через минуту, затем через 5 и 30 минут и через 2 часа — всего до пяти попыток. Идентификатор в X-Autoposter-Delivery при повторах не меняется, поэтому дубли легко отсечь на своей стороне. После исчерпания попыток событие помечается несостоявшимся; текущее состояние всегда можно восстановить через GET /posts или GET /posts/{id}. Код последнего ответа виден как lastStatus в GET /webhooks вместе с lastFiredAt.
Полные примеры тела
{
"event": "POST_PUBLISHED",
"data": {
"postId": "post_777",
"title": "Анонс",
"platform": "VK",
"account": "Афиша города",
"remoteUrl": "https://vk.com/clip-12345678_456239017",
"mediaKind": "clip",
"note": null
},
"timestamp": "2026-08-28T09:00:03.412Z"
}mediaKind — фактический тип видео у ВК: "clip" или "video"; у остальных площадок null. note — оговорка площадки при частичном успехе (сработал фолбэк с Клипа на обычное видео, ролик TikTok ушёл черновиком, потерялось фото); когда всё вышло ровно как просили — null. remoteUrl тоже бывает null: часть площадок отдаёт ссылку не сразу.
{
"event": "POST_FAILED",
"data": {
"postId": "post_778",
"variantId": "var_991",
"accountId": "acc_120",
"title": "Вертикальный ролик",
"platform": "VK",
"account": "Афиша города",
"error": "ВК отклонил публикацию Клипом (Video is too long). Режим clipOnly: обычным видео не публикуем.",
"errorCode": "VK_CLIP_REJECTED",
"summary": "ВКонтакте отклонил ролик как Клип, а режим «только Клип» запрещает публиковать его обычным видео.",
"action": "Проверьте формат: Клипы — вертикальное видео до 3 минут. Либо отключите режим «только Клип».",
"fix": "content",
"attempt": 1,
"willRetry": false
},
"timestamp": "2026-08-28T09:02:41.077Z"
}Ветвление стройте по errorCode (машинный код площадки) или по fix — что нужно сделать: reconnect (переподключить аккаунт), permissions (выдать права), wait (подождать), content (поправить сам пост), support (написать нам), none. Поля summary и action — готовый текст для показа вашему пользователю; error — дословный ответ площадки, на его разбор логику не завязывайте. Событие означает окончательный отказ: повторы исчерпаны (willRetry: false), сервис к этой площадке не вернётся.
Тестовый режим
Встраивая автопостинг в свой продукт, интеграцию нужно прогнать целиком: создать пост, дождаться события, разобрать ответ. Делать это на живых сообществах клиентов — значит мусорить в чужих лентах. Передайте sandbox: true в POST /posts — и пост пройдёт весь путь, кроме последнего шага.
Происходиткак в боюПост создаётся, встаёт в очередь, публикуется по расписанию, меняет статусы (QUEUED → PUBLISHING → PUBLISHED), шлёт вебхуки POST_PUBLISHED с подписью. Ответы по форме неотличимы от боевых — разбор на вашей стороне проверяется тем же кодом.
Не происходит—Обращения к соцсети нет: ничего не публикуется, токены аккаунтов не используются, лимиты площадок не расходуются. Статистика по таким постам не собирается, проверка «вышло ли на самом деле» их не трогает.
remotePostIdsandbox_…Идентификатор начинается с sandbox_ и детерминирован: повторный запрос статуса вернёт тот же, а не новый выдуманный.
urlstringВедёт на наш домен (/sandbox/…), а не на похожий адрес соцсети: ссылки на несуществующую публикацию мы не подсовываем.
sandboxbooleanВозвращается в ответе POST /posts и в GET /posts/{id} — если флаг где-то потерялся по дороге, вы увидите это сразу, а не по факту публикации.
curl -X POST https://autoposter.digital/api/v1/posts \
-H "x-api-key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
-d '{"text":"Проверка интеграции",
"accountIds":["acc_123"],
"sandbox":true}'Тестовые посты видны в кабинете наравне с обычными и так же удаляются. Отдельного ключа для песочницы не нужно: режим включается флагом в запросе, поэтому один и тот же код можно гонять и в тесте, и в бою, меняя единственное поле.
Типичный сценарий: клип из API
- 1.
GET /accounts— взятьidнужных каналов. - 2.
POST /media— получитьuploadUrl, затемPUTзалить видео. - 3.
POST /postsсmediaIds,asClip: trueиidempotencyKey. - 4. (опц.)
POST /webhooksнаPOST_PUBLISHED/POST_FAILED— узнать результат без опроса. - 5.
GET /stats/{postId}— собрать метрики позже; за удержанием и приходом подписчиков —GET /analytics.
Нужен ключ или помощь?
Ключ — в Настройках → API (тариф «Бизнес»). Вопросы — через кнопку «Помощь» в кабинете.