Разработчикам

API followers.vc

Планируйте и публикуйте посты, управляйте каналами и медиафайлами, читайте уведомления и аналитику рабочего пространства followers.vc.

Базовый адрес: https://followers.vc/api/v1 Версия: 1.0.0 OpenAPI 3.1: openapi.json

English version · Документация · Ключи API

Содержание

Публичный API followers.vc, версия 1. Каждый запрос содержит ключ рабочего пространства: Authorization: Bearer fvc_…. Ключи создаются и отзываются на странице API keys рабочего пространства (/app/api-keys); ключ показывается один раз. Ключ действует с правами своего автора в команде и только в тех областях доступа, которые у него есть (ключ без областей может всё). Запросы передаются в JSON (Content-Type: application/json), кроме загрузки файла — она идёт как multipart/form-data; время в ответах — unix-секунды.

Ошибки всегда имеют вид {"error": {"code": "…", "message": "…", "fields": {…}}} и приходят с HTTP-статусом; fields есть только у ошибок валидации. Все списки имеют общую обёртку с data, meta и links; ?page начинается с 1, ?per_page по умолчанию равен 25, максимум — 100.

Быстрый старт

API followers.vc позволяет из вашего кода читать каналы, загружать медиа, создавать посты, проверять их и повторять публикацию. Эта глава проведёт вас от ключа до запланированного поста. Все endpoint, поля и коды ошибок ниже описаны в контракте OpenAPI (openapi.json).

Получите ключ

  • Ключ создаёт владелец команды или участник с правом на API. Тариф команды должен включать API, иначе запросы получат ошибку api_not_in_plan.
  • Ключи создаются на странице API-ключи рабочего пространства: /app/api-keys.
  • Ключ выглядит как fvc_… и показывается один раз. Скопируйте его сразу: хранится только его хеш. Потеряли — отзовите ключ и создайте новый.
  • Ключ действует с правами своего автора в команде и только в тех областях (scopes), которые у него есть. Ключ без областей может всё; по умолчанию на странице отмечены все.
Область Разрешает
channels:read GET /channels, GET /channels/{id}
media:read GET /media, GET /media/{id}
media:write POST /media, POST /media/from-url, DELETE /media/{id}
posts:read GET /posts, GET /posts/{id}
posts:write POST /posts, PATCH /posts/{id}, DELETE /posts/{id}, POST /posts/{id}/retry
notifications:read GET /notifications, GET /notifications/unread-count
notifications:write POST /notifications/{id}/read, POST /notifications/read-all
analytics:read GET /analytics, GET /analytics/{id}

Кроме области, ресурсу нужен соответствующий модуль в тарифе (и в роли автора ключа): каналы, файлы, публикации и аналитика. Без модуля ответ — module_not_in_plan, без области — insufficient_scope.

Базовый адрес и аутентификация

Базовый адрес — https://followers.vc/api/v1. Ключ передаётся в заголовке Authorization, но не в строке запроса:

Authorization: Bearer <key>

Запросы с телом — JSON (Content-Type: application/json), кроме загрузки файла POST /media: она идёт как multipart/form-data. Время в ответах REST — unix-секунды.

Первый запрос

GET /me не требует областей. Он показывает команду, тариф, ваши лимиты и сам ключ:

curl https://followers.vc/api/v1/me \
  -H "Authorization: Bearer fvc_YOUR_KEY"
{
  "data": {
    "team": { "id": "t-91ac0f", "name": "Acme" },
    "plan": { "name": "Pro", "expires_at": 1793000000 },
    "limits": {
      "rate_limit": 60,
      "posts": { "limit": 300, "used": 12, "left": 288 },
      "max_channels": 10
    },
    "key": { "name": "n8n", "prefix": "fvc_a1B2", "scopes": null }
  }
}

rate_limit — запросов в минуту для этого ключа, -1 — без ограничения. scopes: null — все области. plan.expires_at равен null, если тариф не истекает.

Список каналов

Нужна область channels:read. Необязательный фильтр network сужает список.

curl "https://followers.vc/api/v1/channels?network=telegram" \
  -H "Authorization: Bearer fvc_YOUR_KEY"

У канала есть id, network, category, name, username, avatar, status, reason и capabilities (что канал умеет публиковать). status — active, paused или needs_reconnect; для последнего reason объясняет причину, а посты канала ждут. id нужен, чтобы создать пост.

Загрузка медиа по ссылке

Нужна область media:write. Адрес должен быть публичным http или https; адреса внутри частных сетей отклоняются ошибкой 422 по полю url.

curl -X POST https://followers.vc/api/v1/media/from-url \
  -H "Authorization: Bearer fvc_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/photo.png", "name": "launch.png"}'
{
  "data": {
    "id": "fil-5f1c0e2a9b",
    "name": "launch.png",
    "url": "https://followers.vc/storage/files/photo.png",
    "type": "image",
    "mime": "image/png",
    "extension": "png",
    "size": 48213,
    "width": 1080,
    "height": 1080,
    "duration": null,
    "media_status": "ready",
    "media_error": null,
    "created_at": 1793000000
  }
}

media_status — pending, пока размер и длительность читаются в фоне, затем ready или failed (причина в media_error). Чтобы загрузить локальный файл, отправьте POST /media как multipart/form-data с полем file:

curl -X POST https://followers.vc/api/v1/media \
  -H "Authorization: Bearer fvc_YOUR_KEY" \
  -F "file=@./photo.png"

Загрузка может завершиться ошибкой storage_limit (хранилище тарифа заполнено) или file_too_large.

Создание поста

Нужны область posts:write и модуль публикаций в тарифе. В теле запроса:

  • channels — массив id каналов, от 1 до 50; на каждый канал создаётся свой пост;
  • type — text, link или media;
  • caption — текст;
  • link — адрес, для поста типа link;
  • media — массив id медиа из /media по порядку, для поста типа media;
  • options — параметры конкретной сети, в том виде, как их принимает редактор;
  • publish_at — когда публиковать.

По расписанию

publish_at — ISO 8601 (2026-11-05T10:30:00+03:00, …Z) или unix-секунды. Без смещения берётся часовой пояс автора ключа. Время должно быть в будущем.

curl -X POST https://followers.vc/api/v1/posts \
  -H "Authorization: Bearer fvc_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c9a3e-launch-post-1" \
  -d '{
    "channels": ["a8d3f0c2e1b94f7d"],
    "type": "media",
    "caption": "Hello from the API",
    "media": ["fil-5f1c0e2a9b"],
    "publish_at": "2026-11-05T10:30:00+03:00"
  }'

Сейчас

Не передавайте publish_at. Пост считается назначенным на «сейчас» и уходит в очередь публикации; внутри запроса он никогда не публикуется, поэтому в ответе будет статус вроде scheduled, а не результат.

curl -X POST https://followers.vc/api/v1/posts \
  -H "Authorization: Bearer fvc_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channels": ["a8d3f0c2e1b94f7d"], "type": "text", "caption": "Hello from the API"}'

Ответ — 201 с одним постом на канал:

{
  "data": [
    {
      "id": "kQ7x9a1b2c3d",
      "channel": { "id": "a8d3f0c2e1b94f7d", "network": "telegram", "name": "Acme news" },
      "type": "text",
      "caption": "Hello from the API",
      "link": null,
      "media": [],
      "options": {},
      "status": "scheduled",
      "publish_at": 1793000000,
      "attempts": 0
    }
  ]
}

(У поста есть также retry_at, result и url; они пусты, пока сообщать нечего.) Если хотя бы один канал отказал, ничего не сохраняется, а отказ приходит в поле channels.<id> ошибки validation_failed. Исчерпанная квота постов — 403 post_quota.

Idempotency-Key

POST /posts принимает заголовок Idempotency-Key. Тот же ключ с тем же телом в течение 24 часов вернёт первый ответ с заголовком Idempotent-Replayed: true и не создаст пост заново, поэтому повторять запрос после сбоя сети безопасно. Тот же ключ с другим телом — 422 idempotency_key_reused; тот же ключ, пока первый запрос ещё выполняется, — 409 idempotency_key_in_use. Сохраняется только успешный ответ.

Статус поста

Нужна область posts:read.

curl https://followers.vc/api/v1/posts/kQ7x9a1b2c3d \
  -H "Authorization: Bearer fvc_YOUR_KEY"

status — один из draft, scheduled, reconnect_wait (запланирован, но канал нужно сначала переподключить), published, failed. У опубликованного поста url — адрес в сети. В result лежит ответ последней попытки: message, error_class (transient, uncertain — пост мог быть опубликован —, auth или permanent) и social_id. После временной ошибки retry_at показывает, когда очередь попробует снова. GET /posts/{id} возвращает ещё и attempt_log — все попытки от старых к новым с полями attempt, outcome, error_class, message и at.

Список постов — GET /posts с фильтрами status, channel, from и to (по времени публикации, ISO 8601 или unix-секунды).

Повтор неудачного поста

Нужна область posts:write. Повторить можно только пост со статусом failed; любой другой статус даёт 409 not_retryable.

curl -X POST https://followers.vc/api/v1/posts/kQ7x9a1b2c3d/retry \
  -H "Authorization: Bearer fvc_YOUR_KEY"

Ответ — пост в той же форме, что у GET /posts/{id}. Следите за результатом опросом или подпишитесь на события post.published и post.failed (глава «Вебхуки»).

Списки и постраничность

Все списки (/channels, /media, /posts, /notifications) имеют один конверт:

{
  "data": [],
  "meta": { "page": 1, "per_page": 25, "total": 40 },
  "links": {
    "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
    "prev": null
  }
}

?page начинается с 1. ?per_page по умолчанию 25, максимум 100. На последней странице links.next равен null. Ссылки сохраняют per_page и действующие фильтры, так что переходите по ним как есть.

Ошибки

У каждой ошибки одна форма и HTTP-статус:

{
  "error": {
    "code": "validation_failed",
    "message": "…",
    "fields": { "channels": ["The channels field is required."] }
  }
}

fields есть только у ошибок валидации. Ветвитесь по code, а не по message.

Статус Коды
401 unauthenticated — ключа нет, он неизвестен или отозван
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope, storage_limit, file_too_large, post_quota, analytics_not_in_plan
404 not_found, analytics_unavailable
405 method_not_allowed
409 not_editable, not_deletable, not_retryable, idempotency_key_in_use
413 payload_too_large
422 validation_failed, idempotency_key_reused, not_saved
429 rate_limited
500 server_error — подробности никогда не отправляются

Полный каталог — x-error-codes в openapi.json.

Лимит запросов

Лимит считается на ключ в минуту и берётся из тарифа (по умолчанию 60; limits.rate_limit в GET /me). В ответах есть:

  • X-RateLimit-Limit — запросов в минуту для этого ключа; отсутствует, если у тарифа нет лимита;
  • X-RateLimit-Remaining — сколько запросов осталось в текущую минуту.

При превышении API отвечает 429 с кодом rate_limited и заголовком Retry-After (сколько секунд ждать). Подождите это время перед следующим запросом.

OAuth

Приложение может действовать от имени человека в followers.vc без ключа API: оно просит доступ, человек разрешает его на экране согласия, и приложение получает токены для этого человека и команды, в которой он работает. Это OAuth 2.1 с кодом авторизации и PKCE.

Адреса

Что Адрес
Экран согласия (адрес авторизации) https://followers.vc/app/api-keys/authorize
Токен POST https://followers.vc/oauth/token
Отзыв POST https://followers.vc/oauth/revoke
Регистрация клиента им самим POST https://followers.vc/oauth/register
Сведения о сервере GET https://followers.vc/.well-known/oauth-authorization-server
Сведения о ресурсе GET https://followers.vc/.well-known/oauth-protected-resource/api/v1 (и /api/mcp)

Регистрация приложения

В кабинете откройте Ключи API → OAuth-приложения и подключённые приложения и зарегистрируйте приложение: название, которое люди увидят на экране согласия, и адреса возврата, по одному в строке. Адрес возврата — https, или http на localhost и 127.0.0.1 для приложения, которое работает на компьютере человека. Вы получите client_id и client_secret; секрет показывается один раз. Приложение без сервера (CLI, программа для компьютера или телефона) регистрируется как публичное: у него нет секрета, оно подтверждает себя только через PKCE.

Порядок

  1. Сделайте случайный code_verifier (43–128 символов) и его code_challenge = BASE64URL(SHA256(code_verifier)).
  2. Отправьте человека на экран согласия: response_type=code, client_id, redirect_uri, state, scope (через пробел; без него — все области), code_challenge и code_challenge_method=S256. PKCE обязателен для любого приложения.
  3. Человек входит, если нужно, и разрешает или запрещает доступ. Браузер возвращается на адрес возврата с code, state и iss или с error=access_denied.
  4. В течение десяти минут обменяйте код на адресе токена: grant_type=authorization_code, code, redirect_uri, code_verifier, client_id (и client_secret — в теле или через HTTP Basic). Код действует один раз.
  5. В ответе — access_token (fvc_at_…, на час), refresh_token (fvc_rt_…, на 30 дней), expires_in и scope. Вызывайте API с Authorization: Bearer <access_token>, как с ключом.
  6. До истечения часа получите новую пару: grant_type=refresh_token, refresh_token, client_id (и секрет). Старая пара перестаёт работать. Токен обновления, использованный второй раз, прекращает весь доступ приложения: нужно новое согласие.

Ошибки этих адресов — в формате OAuth: {"error": "invalid_grant", "error_description": "…"}.

Области и лимиты

Области — те же, что у ключей API (см. «Быстрый старт»). Токен действует с областями, которые разрешил человек, и с его ролью в команде: у участника с ролью «Пользователь» нет доступа к API, поэтому экран согласия для него закрыт, как и ключи. Лимит запросов тарифа действует на каждый токен.

Отзыв доступа

Приложение отзывает токен на адресе отзыва (token, client_id, секрет): токен доступа и токен обновления этого согласия перестают работать вместе. Человек видит разрешённые им приложения в разделе Подключённые приложения и может отозвать любое. Удаление зарегистрированного приложения прекращает все его токены.

Клиенты MCP

Клиенту сервера MCP (/api/mcp) регистрация не нужна. Без токена сервер отвечает 401 с WWW-Authenticate: Bearer resource_metadata="…"; клиент читает сведения, регистрируется сам на /oauth/register как публичный и открывает экран согласия. На экране согласия сказано, что такое приложение зарегистрировалось само и сайтом не проверено.

Документы метаданных клиента (Client ID Metadata Documents)

Вместо регистрации клиент может взять в качестве client_id https-адрес собственного JSON-документа (например, https://app.example/oauth/client.json; путь обязателен). Адреса авторизации и токена загружают его сами: только публичные адреса, без перенаправлений, 5 секунд, не больше 16 КБ. Документ — JSON-объект, где client_id равен его собственному адресу, client_name не пуст и redirect_uris — непустой список (https или http на localhost); token_endpoint_auth_method может быть только none, client_secret быть не должно. redirect_uri запроса обязан точно совпасть с одним из redirect_uris. PKCE с S256 обязателен, как у любого клиента. Документ кэшируется на срок Cache-Control: max-age (от 5 минут до 24 часов, по умолчанию час). Если он не загрузился или не прошёл проверку, экран согласия показывает ошибку, а адрес токена отвечает invalid_client. Экран согласия показывает имя из документа и хост адреса и сообщает, что сайт клиента не проверял. Динамическая регистрация на /oauth/register работает по-прежнему. Сведения сервера сообщают об обоих способах: client_id_metadata_document_supported и registration_endpoint.

Издатель в ответе

Каждый возврат с адреса авторизации (код, access_denied, любая другая ошибка) содержит iss — издателя ровно так, как он указан в метаданных (RFC 9207); в метаданных есть authorization_response_iss_parameter_supported: true. Клиенту стоит сверять iss с издателем, с которого он начал.

Токен без нужной области

Если у токена нет области, которая нужна запросу, API отвечает 403 insufficient_scope и заголовком WWW-Authenticate: Bearer error="insufficient_scope", scope="<область>", resource_metadata="…". Клиент может снова отправить человека на экран согласия, запросив область из заголовка (вместе с уже имеющимися). Вызов инструмента сервера MCP сообщает тот же отказ в результате инструмента, а не в HTTP.

MCP-сервер

У followers.vc есть MCP-сервер (Model Context Protocol): ИИ-ассистенты, например Claude и Cursor, могут просматривать ваши каналы, загружать медиа, создавать посты, проверять и повторять их. Работает с тем же ключом API, что и REST.

Что это

  • Endpoint: POST https://followers.vc/api/mcp.
  • Транспорт: Streamable HTTP, JSON-RPC 2.0, без состояния (между запросами ничего не хранится, идентификатора сессии нет). Ответ всегда JSON.
  • GET /api/mcp отвечает 405: потока сообщений от сервера нет.
  • Тот же ключ, тот же заголовок (Authorization: Bearer <key>) и тот же лимит запросов, что у REST API. О ключах и лимитах — в главе «Быстрый старт».
  • Или войти через OAuth вместо ключа: клиент, который это умеет (Claude, Cursor и другие), сам находит сервер, открывает экран согласия followers.vc и получает токен для команды, в которой вы работаете. См. главу OAuth.

Версии протокола

Сервер понимает версии 2026-07-28, 2025-11-25, 2025-06-18 и 2025-03-26.

  • 2026-07-28 — без рукопожатия: каждый запрос несёт версию в params._meta["io.modelcontextprotocol/protocolVersion"] и в заголовке MCP-Protocol-Version, метод — в заголовке Mcp-Method, имя инструмента для tools/call — в Mcp-Name. Заголовки должны совпадать с телом, иначе 400 и ошибка -32020. Методы: server/discover (версии, возможности, инструкции), tools/list, tools/call; у каждого результата resultType: "complete" и сведения о сервере в _meta["io.modelcontextprotocol/serverInfo"], у tools/list и server/discover — ttlMs и cacheScope. Неизвестный метод — 404 и -32601.
  • 2025-11-25 и раньше — рукопожатие initialize выбирает версию, дальше ping, tools/list, tools/call.

Версия, которую сервер не знает, — 400 и ошибка -32022 со списком поддерживаемых в error.data.supported.

Области и тариф

Инструменты выполняют те же действия, что и маршруты REST, и проверяют то же самое: модуль в тарифе (и в роли автора ключа) и область ключа. Ключ с областями channels:read, media:read, media:write, posts:read, posts:write, notifications:read, notifications:write и analytics:read может пользоваться всеми инструментами. Ключ с меньшим набором тоже работает: недоступный ему инструмент отвечает ошибкой (см. «Ошибки»).

Инструменты

Инструмент Что делает Область
list_channels Список каналов команды: id (нужен для create_post), сеть, статус (active, paused, needs_reconnect с причиной) и что канал умеет публиковать. Необязательный network. channels:read
upload_media_from_url Скачивает изображение или видео с публичного http(s)-адреса в медиатеку; возвращает id медиа. Принимает url (обязательный) и name. media:write
create_post Создаёт пост на одном или нескольких каналах, по посту на канал. Принимает channels (обязательный, от 1 до 50 id), type (text, link, media; обязательный), caption (обязательный), link, media (id из upload_media_from_url, до 50) и publish_at. Без publish_at пост ставится в очередь на публикацию сейчас. posts:write
list_posts Список постов, сначала с самым поздним временем публикации, со статусом и ошибкой неудачного. Необязательные status (draft, scheduled, reconnect_wait, published, failed), channel, from, to и limit (от 1 до 50, по умолчанию 20). posts:read
get_post Один пост по id: статус, ответ сети и журнал попыток (attempt_log). posts:read
retry_post Ставит неудачный пост (по id) в очередь на повторную публикацию сейчас, со свежим счётчиком попыток. Только пост со статусом failed. posts:write
get_account Показывает, от чьего имени действует ключ: команда, тариф и срок его действия, лимиты (использовано и осталось постов, число каналов, запросов в минуту) и сам ключ с областями. Без аргументов. не нужна
get_channel Один канал по id: сеть, статус и всё, что он публикует, включая пределы для изображений и видео. channels:read
get_next_slot Ближайший свободный слот публикации одного канала (id): момент его расписания, на который ещё нет запланированного поста, в ISO 8601; подставьте его в publish_at. У канала без слотов или без свободного слота в ближайшие 60 дней — null. channels:read
list_sets Список наборов каналов: id, название, id каналов набора (передайте их в create_post) и шаблон (текст, ссылка, id файлов, параметры). Необязательный limit (от 1 до 50, по умолчанию 20). channels:read
list_media Список файлов медиатеки, сначала новые. Необязательные type (image, video) и limit (от 1 до 50, по умолчанию 20). media:read
get_media Один файл медиатеки по id: адрес, тип, размер, разрешение, длина и статус обработки. media:read
delete_media Удаляет файл (по id) из медиатеки и хранилища. Необратимо; разрушающее действие. media:write
update_post Меняет запланированный пост (по id), который сейчас не публикуется. Передавайте только то, что меняется: publish_at, caption, options. posts:write
delete_post Удаляет черновик, запланированный или неудачный пост (по id). Опубликованный пост удалить нельзя. Необратимо; разрушающее действие. posts:write
link_post Для неудачного поста с неопределённой ошибкой: принимает id и url поста, который всё-таки вышел, и помечает пост опубликованным. posts:write
list_notifications Список уведомлений автора ключа, сначала новые. Необязательные unread (true) и limit (от 1 до 50, по умолчанию 20). notifications:read
mark_notifications_read Отмечает прочитанным одно уведомление (id) или все, если id не указан. notifications:write
list_analytics Список каналов команды, у которых есть отчёт аналитики, по сетям. analytics:read
get_channel_analytics Отчёт аналитики одного канала (id) за период: необязательные from и to в ISO 8601 (по умолчанию последние 30 дней, не больше 365). Поля отчёта зависят от сети. analytics:read

Модули тарифа: инструментам для каналов (list_channels, get_channel, get_next_slot) нужен модуль каналов, list_sets — модуль наборов, для медиа (upload_media_from_url, list_media, get_media, delete_media) — модуль файлов, для постов (create_post, list_posts, get_post, retry_post, update_post, delete_post, link_post) — модуль публикаций, для аналитики (list_analytics, get_channel_analytics) — модуль аналитики. get_account, list_notifications и mark_notifications_read модуля не требуют, а get_account не требует и области — как их маршруты REST.

Инструменты, которые удаляют (delete_media, delete_post), помечены как разрушающие, а те, что меняют данные, — как не только для чтения, поэтому ассистент спрашивает человека, прежде чем их вызвать.

Каждый результат содержит короткий текст для ассистента и данные в structuredContent; поля поста те же, что в REST API (id, channel, type, caption, link, media, status, publish_at, retry_at, attempts, result, url).

Время в результатах и аргументах инструментов — ISO 8601. В результатах используется часовой пояс автора ключа (например, 2026-11-05T10:30:00+03:00). publish_at без смещения тоже читается в этом поясе и должен быть в будущем.

Ошибки

Отказ приходит как обычный результат инструмента с isError: true, а не как ошибка JSON-RPC. Текст начинается с кода ошибки API, затем сообщение, а для ошибок валидации — по строке на поле:

{
  "content": [
    {
      "type": "text",
      "text": "insufficient_scope: The key does not carry the scope of the operation."
    }
  ],
  "isError": true
}

Коды те же, что у REST API (insufficient_scope, module_not_in_plan, validation_failed, not_found, not_retryable, post_quota, rate_limited и другие). Неизвестное имя инструмента — ошибка JSON-RPC.

Подключение клиента

Создайте ключ на /app/api-keys с областями из раздела выше. Те же подсказки, с кнопкой копирования адреса, есть на этой странице. Замените <key> своим ключом.

Claude Code

claude mcp add --transport http followers https://followers.vc/api/mcp --header "Authorization: Bearer <key>"

Claude Desktop

В claude_desktop_config.json, через мост mcp-remote (нужен Node.js):

{
  "mcpServers": {
    "followers": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://followers.vc/api/mcp",
        "--header",
        "Authorization:${FOLLOWERS_AUTH}"
      ],
      "env": {
        "FOLLOWERS_AUTH": "Bearer <key>"
      }
    }
  }
}

Cursor

В .cursor/mcp.json или в глобальном ~/.cursor/mcp.json:

{
  "mcpServers": {
    "followers": {
      "url": "https://followers.vc/api/mcp",
      "headers": {
        "Authorization": "Bearer <key>"
      }
    }
  }
}

Проверка вручную

curl -X POST https://followers.vc/api/mcp \
  -H "Authorization: Bearer fvc_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'

Вебхуки: подпись, ответы и повторы

followers.vc может уведомлять ваш сервер о том, что происходит с вашими постами и каналами. Создайте вебхук на странице Webhooks вашего рабочего пространства (/app/webhooks): укажите адрес https, выберите события и, при необходимости, каналы. Вебхуки входят не во все тарифы; тариф также задаёт, сколько активных вебхуков может быть в рабочем пространстве.

События

Событие Когда
post.scheduled Пост запланирован на определённое время (одно событие на канал и время).
post.published Пост опубликован в соцсети.
post.failed Публикация поста окончательно не удалась: автоматических попыток больше не будет.
channel.disconnected Канал потерял доступ и требует переподключения; его посты ждут.
approval.approved Проверяющий одобрил пост на странице согласования.
approval.changes_requested Проверяющий запросил правки поста на странице согласования.
ping Отправляется только кнопкой Send test.

Вебхуки, ограниченные несколькими каналами, получают события постов и каналов только этих каналов.

Запрос

Каждое событие — это POST с телом в JSON:

{
  "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
  "event": "post.published",
  "created_at": "2026-10-16T09:30:00Z",
  "data": {
    "post": {
      "id": "kQ7x9a1",
      "status": "published",
      "type": "media",
      "time_post": "2026-10-16T09:30:00Z",
      "url": "https://t.me/example/123"
    },
    "channel": { "id": "a8d3f0c2", "network": "telegram", "name": "Example" }
  }
}
  • id идентифицирует событие. Он остаётся прежним при повторе события и при ручной повторной отправке, поэтому по нему можно пропускать дубликаты.
  • data.post.status принимает значения scheduled, published или failed; url заполнен у опубликованного поста, если соцсеть возвращает ссылку.
  • post.failed добавляет "error": {"class": "permanent", "message": "…"}. Класс — одно из permanent, transient, uncertain (пост мог быть опубликован), auth.
  • У channel.disconnected тело "data": {"channel": {…}, "reason": "…"}.
  • У approval.approved и approval.changes_requested тело "data": {"approval": {"id": "…", "status": "approved", "decided_by": "…", "decided_at": "…", "comment": "…"}, "posts": [{"post": {…}, "channel": {…}}, …]} — по одному элементу на каждый канал, который охватывает согласование; post.status может быть draft. Вебхук, ограниченный несколькими каналами, получает событие, если согласование охватывает любой из них.
  • У ping тело "data": {"webhook": {"id": "…", "events": […]}, "message": "…"}.

Токены, пароли и другие секреты никогда не передаются.

Заголовки:

Заголовок Значение
Content-Type application/json
User-Agent followers.vc-Webhooks/1.0
X-Followers-Event событие, например post.published
X-Followers-Delivery идентификатор этой доставки (при ручной повторной отправке новый)
X-Followers-Timestamp Unix-время подписи запроса
X-Followers-Signature t=<timestamp>,v1=<signature>

Проверка подписи

У каждого вебхука есть секрет (whsec_…). Он показывается один раз — при создании вебхука или при замене секрета. Подпись — это HMAC-SHA256 от <timestamp>.<raw body> с этим секретом в шестнадцатеричном виде.

  1. Прочитайте необработанное тело запроса до любого разбора JSON.
  2. Возьмите t и v1 из X-Followers-Signature.
  3. Вычислите hex(hmac_sha256(secret, t + "." + body)) и сравните с v1 за постоянное время.
  4. Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.

PHP:

$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_FOLLOWERS_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '').'.'.$body, getenv('FOLLOWERS_WEBHOOK_SECRET'));

if (! isset($sig['t'], $sig['v1']) || ! hash_equals($expected, $sig['v1']) || abs(time() - (int) $sig['t']) > 300) {
    http_response_code(400);
    exit;
}

Node.js:

const crypto = require('crypto');

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  return fresh && parts.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Ответ и повторы

Отвечайте любым кодом 2xx в течение 10 секунд, а работу выполняйте уже после ответа. Тело вашего ответа не используется (первый килобайт сохраняется в журнале доставок). Редиректы не выполняются.

  • Нет ответа, тайм-аут, 429 или 5xx: запрос повторяется через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов, после чего доставка помечается как неудачная.
  • Любой другой ответ (4xx, 3xx): доставка сразу считается неудачной, без повторов.
  • После 20 неудачных запросов подряд вебхук отключается, а владелец рабочего пространства получает уведомление. Включите его снова на странице Webhooks, когда получатель заработает; одна успешная доставка сбрасывает счётчик.

Журнал доставок каждого вебхука хранит все доставки за 30 дней, с кодом ответа, временем и ошибкой, и позволяет отправить доставку повторно. Замена секрета вступает в силу сразу.

Адрес должен быть публичным: адреса в частных сетях, localhost и обычный http отклоняются — и при сохранении вебхука, и перед каждым запросом.

Аутентификация

Ключ рабочего пространства fvc_ + 40 символов в заголовке Authorization: Bearer <key> (никогда не в строке запроса). Области доступа: channels:read, media:read, media:write, posts:read, posts:write, notifications:read, notifications:write, analytics:read.

curl https://followers.vc/api/v1/me \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ошибки

У всех ошибок одинаковое тело: код постоянен, сообщение — для людей.

{"error": {"code": "validation_failed", "message": "The given data was invalid.", "fields": {"caption": ["The caption field must be a string."]}}}
СтатусКодЗначение
401unauthenticatedКлюч не передан, неизвестен или отозван, либо его автор больше не активен в команде.
403api_not_in_planТариф команды не включает доступ к API.
403plan_expiredТариф владельца команды истёк.
403module_not_in_planТариф команды или роль автора ключа в ней не включает этот ресурс.
403insufficient_scopeУ ключа нет области доступа, нужной для операции.
403storage_limitХранилище медиафайлов по тарифу заполнено.
403file_too_largeФайл больше, чем допускает тариф.
403post_quotaКвота постов по тарифу исчерпана.
403analytics_not_in_planТариф не включает аналитику для соцсети этого канала.
404not_foundТакой записи нет в команде ключа (или среди уведомлений его автора).
404analytics_unavailableУ соцсети этого канала нет аналитики.
405method_not_allowedПуть существует, а метод — нет.
409not_editableИзменить можно только запланированный пост, который сейчас не публикуется.
409not_deletableУдалить можно только черновик, запланированный пост или пост с ошибкой, который сейчас не публикуется.
409not_retryableПовторно опубликовать можно только пост с ошибкой.
409not_linkableПривязать к записи в сети можно только неудачный пост с неясной ошибкой.
409idempotency_key_in_useЗапрос с этим Idempotency-Key ещё обрабатывается.
413payload_too_largeТело запроса больше, чем принимает сервер.
422validation_failedЗапрос некорректен; в fields перечислены поля с их сообщениями.
422idempotency_key_reusedЭтот Idempotency-Key уже использовался с другим запросом.
422not_savedПост не сохранён по причине, не связанной с полем.
429rate_limitedСлишком много запросов для этого ключа; подождите Retry-After секунд.
500server_errorЧто-то пошло не так на нашей стороне. Подробности не передаются.
502provider_errorНе удалось запросить отчёт у соцсети. Повторите позже.

Лимиты запросов

ЗаголовокЗначение
X-RateLimit-LimitЗапросов в минуту для этого ключа (определяется тарифом, по умолчанию 60). Отсутствует, если тариф не задаёт лимит.
X-RateLimit-RemainingЗапросов осталось в текущей минуте.
Retry-AfterСколько секунд ждать до следующего запроса.
WWW-AuthenticateBearer.
Idempotent-Replayedtrue, если ответ — сохранённый первый ответ для этого Idempotency-Key.

Account

Команда, тариф и ключ, от имени которых выполняется запрос.

GET /me От чьего имени действует ключ

любой ключ

Команда ключа, её тариф, лимиты и сам ключ. Область доступа не требуется.

Пример запроса

curl -X GET https://followers.vc/api/v1/me \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Команда, тариф, лимиты и ключ.
{
    "data": {
        "team": {
            "id": "t-91ac0f",
            "name": "Acme"
        },
        "plan": {
            "name": "Pro",
            "expires_at": 1793000000
        },
        "limits": {
            "rate_limit": 60,
            "posts": {
                "limit": 300,
                "used": 12,
                "left": 288
            },
            "max_channels": 10
        },
        "key": {
            "name": "n8n",
            "prefix": "fvc_a1B2",
            "scopes": null
        }
    }
}
Поля
ПолеТипОписание
dataMe
data.teamobject
data.team.idstringПубличный идентификатор команды.
data.team.namestring
data.planobject
data.plan.namestring | null
data.plan.expires_atinteger | nullКогда истекает тариф; null — не истекает. Unix-секунды.
data.limitsobject
data.limits.rate_limitintegerЗапросов в минуту для этого ключа; -1 — без ограничения.
data.limits.postsobject
data.limits.posts.limitinteger-1 — без ограничения.
data.limits.posts.usedinteger
data.limits.posts.leftinteger
data.limits.max_channelsinteger-1 — без ограничения.
data.keyobject
data.key.namestring
data.key.prefixstringПервые 8 символов ключа.
data.key.scopesarray of string | nullnull — все области доступа.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired
429 rate_limited
500 server_error

Channels

Аккаунты соцсетей, подключённые к рабочему пространству.

GET /channels Список каналов

область channels:read тариф: appchannels

Каналы команды, сначала старые, с их состоянием и тем, что публикует каждый из них.

Параметры

ИмяГдеТипОписание
network query string Только каналы этой соцсети, например telegram. At most 50 characters.
page query integer Страница, начиная с 1. Minimum 1. Default 1.
per_page query integer Число элементов на странице; значение больше 100 урезается до 100. Minimum 1. Default 25.

Пример запроса

curl -X GET https://followers.vc/api/v1/channels \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Страница каналов.
{
    "data": [
        {
            "id": "a8d3f0c2e1b94f7d",
            "network": "telegram",
            "category": "channel",
            "name": "Acme news",
            "username": "acmenews",
            "avatar": "https://followers.vc/storage/avatar.png",
            "status": "active",
            "reason": null,
            "capabilities": {
                "types": [
                    "text",
                    "link",
                    "media"
                ],
                "max_length": {
                    "text": 4096,
                    "link": 4096,
                    "media": 1024
                },
                "max_media": 10,
                "images": true,
                "videos": true,
                "files": false,
                "mixed": false,
                "limits": {
                    "image": null,
                    "video": null
                }
            }
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 40
    },
    "links": {
        "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
        "prev": null
    }
}
Поля
ПолеТипОписание
dataarray of Channel
data[].idstringПубличный идентификатор канала.
data[].networkstringСоцсеть.
data[].categorystring | nullprofile, page, channel, group…
data[].namestring | null
data[].usernamestring | null
data[].avatarstring | nullАдрес аватара.
data[].statusstringneeds_reconnect — канал потерял доступ; его посты ждут. One of active, paused, needs_reconnect.
data[].reasonstring | nullПочему канал нужно переподключить; для любого другого статуса — null.
data[].capabilitiesChannelCapabilities | nullЧто публикует канал.
data[].capabilities.typesarray of stringТипы постов, которые публикует канал.
data[].capabilities.max_lengthobjectМаксимальная длина текста по типам постов, в символах; null — без ограничения.
data[].capabilities.max_mediaintegerЧисло файлов в одном посте.
data[].capabilities.imagesbooleanПринимает изображения.
data[].capabilities.videosbooleanПринимает видео.
data[].capabilities.filesbooleanПринимает другие файлы (аудио, документы).
data[].capabilities.mixedbooleanИзображения и видео в одном посте.
data[].capabilities.limitsobject
data[].capabilities.limits.imageobject
data[].capabilities.limits.image.max_bytesinteger | nullМаксимальный размер изображения; null — неизвестен.
data[].capabilities.limits.image.aspectAspectRange | nullСамое узкое и самое широкое соотношение, ширина / высота.
data[].capabilities.limits.image.aspect.minnumber
data[].capabilities.limits.image.aspect.maxnumber
data[].capabilities.limits.videoobject
data[].capabilities.limits.video.max_bytesinteger | nullМаксимальный размер видео.
data[].capabilities.limits.video.min_secondsnumber | nullМинимальная длительность видео.
data[].capabilities.limits.video.max_secondsnumber | nullМаксимальная длительность видео.
data[].capabilities.limits.video.aspectAspectRange | nullСамое узкое и самое широкое соотношение, ширина / высота.
data[].capabilities.limits.video.aspect.minnumber
data[].capabilities.limits.video.aspect.maxnumber
data[].capabilities.limits.video.min_widthinteger | null
data[].capabilities.limits.video.min_heightinteger | null
data[].capabilities.limits.video.max_widthinteger | null
data[].capabilities.limits.video.max_heightinteger | null
data[].capabilities.limits.video.codecsarray of string | nullВидеокодеки в том виде, как их называет ffprobe.
metaobject
meta.pageintegerТекущая страница, начиная с 1.
meta.per_pageintegerЭлементов на странице (по умолчанию 25, не более 100).
meta.totalintegerЭлементов на всех страницах.
linksobject
links.nextstring | nullСледующая страница; на последней — null.
links.prevstring | nullПредыдущая страница; на первой — null.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
422 validation_failed
429 rate_limited
500 server_error

GET /channels/{id} Получить канал

область channels:read тариф: appchannels

Один канал команды.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор канала.

Пример запроса

curl -X GET https://followers.vc/api/v1/channels/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Канал.
{
    "data": {
        "id": "a8d3f0c2e1b94f7d",
        "network": "telegram",
        "category": "channel",
        "name": "Acme news",
        "username": "acmenews",
        "avatar": "https://followers.vc/storage/avatar.png",
        "status": "active",
        "reason": null,
        "capabilities": {
            "types": [
                "text",
                "link",
                "media"
            ],
            "max_length": {
                "text": 4096,
                "link": 4096,
                "media": 1024
            },
            "max_media": 10,
            "images": true,
            "videos": true,
            "files": false,
            "mixed": false,
            "limits": {
                "image": {
                    "max_bytes": 10485760,
                    "aspect": null
                },
                "video": {
                    "max_bytes": 52428800,
                    "min_seconds": 3,
                    "max_seconds": 90,
                    "aspect": null,
                    "min_width": null,
                    "min_height": null,
                    "max_width": 1920,
                    "max_height": 1920,
                    "codecs": [
                        "h264",
                        "hevc"
                    ]
                }
            }
        }
    }
}
Поля
ПолеТипОписание
dataChannel
data.idstringПубличный идентификатор канала.
data.networkstringСоцсеть.
data.categorystring | nullprofile, page, channel, group…
data.namestring | null
data.usernamestring | null
data.avatarstring | nullАдрес аватара.
data.statusstringneeds_reconnect — канал потерял доступ; его посты ждут. One of active, paused, needs_reconnect.
data.reasonstring | nullПочему канал нужно переподключить; для любого другого статуса — null.
data.capabilitiesChannelCapabilities | nullЧто публикует канал.
data.capabilities.typesarray of stringТипы постов, которые публикует канал.
data.capabilities.max_lengthobjectМаксимальная длина текста по типам постов, в символах; null — без ограничения.
data.capabilities.max_mediaintegerЧисло файлов в одном посте.
data.capabilities.imagesbooleanПринимает изображения.
data.capabilities.videosbooleanПринимает видео.
data.capabilities.filesbooleanПринимает другие файлы (аудио, документы).
data.capabilities.mixedbooleanИзображения и видео в одном посте.
data.capabilities.limitsobject
data.capabilities.limits.imageobject
data.capabilities.limits.image.max_bytesinteger | nullМаксимальный размер изображения; null — неизвестен.
data.capabilities.limits.image.aspectAspectRange | nullСамое узкое и самое широкое соотношение, ширина / высота.
data.capabilities.limits.image.aspect.minnumber
data.capabilities.limits.image.aspect.maxnumber
data.capabilities.limits.videoobject
data.capabilities.limits.video.max_bytesinteger | nullМаксимальный размер видео.
data.capabilities.limits.video.min_secondsnumber | nullМинимальная длительность видео.
data.capabilities.limits.video.max_secondsnumber | nullМаксимальная длительность видео.
data.capabilities.limits.video.aspectAspectRange | nullСамое узкое и самое широкое соотношение, ширина / высота.
data.capabilities.limits.video.aspect.minnumber
data.capabilities.limits.video.aspect.maxnumber
data.capabilities.limits.video.min_widthinteger | null
data.capabilities.limits.video.min_heightinteger | null
data.capabilities.limits.video.max_widthinteger | null
data.capabilities.limits.video.max_heightinteger | null
data.capabilities.limits.video.codecsarray of string | nullВидеокодеки в том виде, как их называет ffprobe.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
429 rate_limited
500 server_error

GET /channels/{id}/next-slot Ближайший свободный слот канала

область channels:read тариф: appchannels

Ближайший свободный слот публикации одного канала: время, которое редактор подставляет в поле времени для «ближайшего свободного слота». Слот занят, если у канала уже есть запланированный пост на эту минуту. У канала без слотов или без свободного слота в ближайшие 60 дней next_slot: null.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор канала.

Пример запроса

curl -X GET https://followers.vc/api/v1/channels/{id}/next-slot \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Слот.
{
    "data": {
        "channel": "a8d3f0c2e1b94f7d",
        "next_slot": 1792569600,
        "slots": {
            "times": [
                "09:00"
            ],
            "days": [
                1
            ],
            "timezone": "Europe/Moscow"
        }
    }
}
Поля
ПолеТипОписание
dataNextSlot
data.channelstringПубличный идентификатор канала.
data.next_slotinteger | nullUnix-время ближайшего свободного слота; null, если у канала нет слотов или свободного нет в ближайшие 60 дней.
data.slotsPostingSlots | nullСлоты публикации канала: время суток в выбранные дни недели в одном часовом поясе.
data.slots.timesarray of stringВремя суток, HH:MM.
data.slots.daysarray of integerДни недели по ISO, понедельник — 1.
data.slots.timezonestringЧасовой пояс времени.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
429 rate_limited
500 server_error

GET /sets Список наборов

область channels:read тариф: appgroups

Наборы каналов команды, сначала недавно изменённые: название, каналы набора и шаблон (текст, ссылка, файлы, параметры), который редактор подставляет в пустые поля.

Пример запроса

curl -X GET https://followers.vc/api/v1/sets \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Страница наборов.
{
    "data": [
        {
            "id": "b7e4c1a9d2f0",
            "name": "Shops",
            "color": "primary",
            "channels": [
                "string"
            ],
            "template": {
                "caption": "string",
                "link": "string",
                "media": [
                    null
                ],
                "options": {}
            }
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 40
    },
    "links": {
        "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
        "prev": null
    }
}
Поля
ПолеТипОписание
dataarray of ChannelSet
data[].idstringПубличный идентификатор набора.
data[].namestring | null
data[].colorstring | nullЦвет набора в кабинете.
data[].channelsarray of stringПубличные идентификаторы каналов набора; передайте их в POST /posts.
data[].templateSetTemplate | nullЧто редактор подставляет в пустые поля при выборе набора; null у набора без шаблона.
data[].template.captionstringТекст.
data[].template.linkstringСсылка; пусто, если её нет.
data[].template.mediaarray of stringИдентификаторы файлов, как в /media.
data[].template.optionsobjectПараметры соцсетей, например fb_type.
metaobject
meta.pageintegerТекущая страница, начиная с 1.
meta.per_pageintegerЭлементов на странице (по умолчанию 25, не более 100).
meta.totalintegerЭлементов на всех страницах.
linksobject
links.nextstring | nullСледующая страница; на последней — null.
links.prevstring | nullПредыдущая страница; на первой — null.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
422 validation_failed
429 rate_limited
500 server_error

Media

Медиатека: файлы для вложения в посты.

GET /media Список медиафайлов

область media:read тариф: appfiles

Файлы медиатеки команды, сначала новые. Папки не показываются.

Параметры

ИмяГдеТипОписание
type query string Только изображения или только видео. One of image, video.
page query integer Страница, начиная с 1. Minimum 1. Default 1.
per_page query integer Число элементов на странице; значение больше 100 урезается до 100. Minimum 1. Default 25.

Пример запроса

curl -X GET https://followers.vc/api/v1/media \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Страница файлов.
{
    "data": [
        {
            "id": "fil-5f1c0e2a9b",
            "name": "photo_x1Y2z3.png",
            "url": "https://followers.vc/storage/files/photo.png",
            "type": "image",
            "mime": "image/png",
            "extension": "png",
            "size": 48213,
            "width": 1080,
            "height": 1080,
            "duration": null,
            "media_status": "ready",
            "media_error": null,
            "created_at": 1793000000
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 40
    },
    "links": {
        "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
        "prev": null
    }
}
Поля
ПолеТипОписание
dataarray of Media
data[].idstringПубличный идентификатор файла.
data[].namestring
data[].urlstringПубличный адрес файла.
data[].typestring | nullВид файла: image, video, pdf, doc, csv…
data[].mimestring | null
data[].extensionstring | null
data[].sizeintegerБайты.
data[].widthinteger | nullПиксели; null — неизвестно.
data[].heightinteger | nullПиксели; null — неизвестно.
data[].durationnumber | nullСекунды, для видео.
data[].media_statusstring | nullФоновая обработка файла: pending, ready или failed; null — файл не обрабатывался (документ или более старый файл). One of pending, ready, failed.
data[].media_errorstring | nullПочему файл не удалось обработать.
data[].created_atinteger | nullВремя загрузки. Unix-секунды.
metaobject
meta.pageintegerТекущая страница, начиная с 1.
meta.per_pageintegerЭлементов на странице (по умолчанию 25, не более 100).
meta.totalintegerЭлементов на всех страницах.
linksobject
links.nextstring | nullСледующая страница; на последней — null.
links.prevstring | nullПредыдущая страница; на первой — null.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
422 validation_failed
429 rate_limited
500 server_error

POST /media Загрузить файл

область media:write тариф: appfiles

Сохраняет загруженный файл в корне медиатеки. Допустимые типы, максимальный размер файла и объём хранилища определяются тарифом, как и в кабинете. Видео обрабатывается в фоне (media_status).

Тело запроса multipart/form-data

ПолеТипОписание
file обязательно file Файл.

Пример запроса

curl -X POST https://followers.vc/api/v1/media \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY" \
  -F "file=@photo.png"

Ответ 201

Сохранённый файл.
{
    "data": {
        "id": "fil-5f1c0e2a9b",
        "name": "photo_x1Y2z3.png",
        "url": "https://followers.vc/storage/files/photo.png",
        "type": "image",
        "mime": "image/png",
        "extension": "png",
        "size": 48213,
        "width": 1080,
        "height": 1080,
        "duration": null,
        "media_status": "ready",
        "media_error": null,
        "created_at": 1793000000
    }
}
Поля
ПолеТипОписание
dataMedia
data.idstringПубличный идентификатор файла.
data.namestring
data.urlstringПубличный адрес файла.
data.typestring | nullВид файла: image, video, pdf, doc, csv…
data.mimestring | null
data.extensionstring | null
data.sizeintegerБайты.
data.widthinteger | nullПиксели; null — неизвестно.
data.heightinteger | nullПиксели; null — неизвестно.
data.durationnumber | nullСекунды, для видео.
data.media_statusstring | nullФоновая обработка файла: pending, ready или failed; null — файл не обрабатывался (документ или более старый файл). One of pending, ready, failed.
data.media_errorstring | nullПочему файл не удалось обработать.
data.created_atinteger | nullВремя загрузки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope, storage_limit, file_too_large
413 payload_too_large
422 validation_failed
429 rate_limited
500 server_error

POST /media/from-url Сохранить файл по адресу

область media:write тариф: appfiles

Скачивает файл по публичному адресу http/https в медиатеку. Адреса внутри частных сетей отклоняются (422 по полю url).

Тело запроса application/json

ПолеТипОписание
url обязательно string (uri) Публичный адрес http или https. At most 2048 characters.
name string | null Имя, под которым сохранить файл. At most 255 characters.

Пример запроса

curl -X POST https://followers.vc/api/v1/media/from-url \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/photo.png","name":"launch.png"}'

Ответ 201

Сохранённый файл.
{
    "data": {
        "id": "fil-5f1c0e2a9b",
        "name": "photo_x1Y2z3.png",
        "url": "https://followers.vc/storage/files/photo.png",
        "type": "image",
        "mime": "image/png",
        "extension": "png",
        "size": 48213,
        "width": 1080,
        "height": 1080,
        "duration": null,
        "media_status": "ready",
        "media_error": null,
        "created_at": 1793000000
    }
}
Поля
ПолеТипОписание
dataMedia
data.idstringПубличный идентификатор файла.
data.namestring
data.urlstringПубличный адрес файла.
data.typestring | nullВид файла: image, video, pdf, doc, csv…
data.mimestring | null
data.extensionstring | null
data.sizeintegerБайты.
data.widthinteger | nullПиксели; null — неизвестно.
data.heightinteger | nullПиксели; null — неизвестно.
data.durationnumber | nullСекунды, для видео.
data.media_statusstring | nullФоновая обработка файла: pending, ready или failed; null — файл не обрабатывался (документ или более старый файл). One of pending, ready, failed.
data.media_errorstring | nullПочему файл не удалось обработать.
data.created_atinteger | nullВремя загрузки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope, storage_limit, file_too_large
422 validation_failed
429 rate_limited
500 server_error

GET /media/{id} Получить файл

область media:read тариф: appfiles

Один файл медиатеки.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор файла.

Пример запроса

curl -X GET https://followers.vc/api/v1/media/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Файл.
{
    "data": {
        "id": "fil-5f1c0e2a9b",
        "name": "photo_x1Y2z3.png",
        "url": "https://followers.vc/storage/files/photo.png",
        "type": "image",
        "mime": "image/png",
        "extension": "png",
        "size": 48213,
        "width": 1080,
        "height": 1080,
        "duration": null,
        "media_status": "ready",
        "media_error": null,
        "created_at": 1793000000
    }
}
Поля
ПолеТипОписание
dataMedia
data.idstringПубличный идентификатор файла.
data.namestring
data.urlstringПубличный адрес файла.
data.typestring | nullВид файла: image, video, pdf, doc, csv…
data.mimestring | null
data.extensionstring | null
data.sizeintegerБайты.
data.widthinteger | nullПиксели; null — неизвестно.
data.heightinteger | nullПиксели; null — неизвестно.
data.durationnumber | nullСекунды, для видео.
data.media_statusstring | nullФоновая обработка файла: pending, ready или failed; null — файл не обрабатывался (документ или более старый файл). One of pending, ready, failed.
data.media_errorstring | nullПочему файл не удалось обработать.
data.created_atinteger | nullВремя загрузки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
429 rate_limited
500 server_error

DELETE /media/{id} Удалить файл

область media:write тариф: appfiles

Удаляет файл и его сохранённую копию.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор файла.

Пример запроса

curl -X DELETE https://followers.vc/api/v1/media/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 204

Удалено.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
429 rate_limited
500 server_error

Posts

Создание, планирование, изменение и повторная отправка постов.

GET /posts Список постов

область posts:read тариф: apppublishing

Посты команды, сначала с самым поздним временем публикации.

Параметры

ИмяГдеТипОписание
status query string Только посты с этим статусом. One of draft, scheduled, reconnect_wait, published, failed.
channel query string Публичный идентификатор канала. At most 64 characters.
from query string | integer Время публикации от (включительно).
to query string | integer Время публикации до (включительно).
page query integer Страница, начиная с 1. Minimum 1. Default 1.
per_page query integer Число элементов на странице; значение больше 100 урезается до 100. Minimum 1. Default 25.

Пример запроса

curl -X GET https://followers.vc/api/v1/posts \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Страница постов.
{
    "data": [
        {
            "id": "kQ7x9a1b2c3d",
            "channel": {
                "id": "a8d3f0c2e1b94f7d",
                "network": "telegram",
                "name": "Acme news"
            },
            "type": "text",
            "caption": "Hello from the API",
            "thread": [
                "string"
            ],
            "thread_delay": 0,
            "link": null,
            "media": [
                {
                    "id": "fil-5f1c0e2a9b",
                    "url": "https://followers.vc/storage/files/photo.png",
                    "alt": null
                }
            ],
            "options": {},
            "status": "scheduled",
            "publish_at": 1793000000,
            "retry_at": 1793000000,
            "attempts": 0,
            "result": {
                "message": "Bad token",
                "error_class": "permanent",
                "social_id": null,
                "thread": [
                    null
                ]
            },
            "url": null,
            "metrics": {
                "likes": 42,
                "reposts": 7,
                "comments": 5,
                "quotes": 1,
                "views": 1800,
                "updated_at": 1793003600
            },
            "created_at": 1793000000
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 40
    },
    "links": {
        "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
        "prev": null
    }
}
Поля
ПолеТипОписание
dataarray of Post
data[].idstringПубличный идентификатор поста.
data[].channelobject
data[].channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data[].channel.networkstring | null
data[].channel.namestring | null
data[].typestringtext, link или media (у более старых постов могут быть другие типы).
data[].captionstring
data[].threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data[].thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data[].linkstring | null
data[].mediaarray of objectФайлы поста по порядку.
data[].media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data[].media[].urlstring
data[].media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data[].optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data[].statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data[].publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data[].retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data[].attemptsintegerЧисло попыток публикации на данный момент.
data[].resultPostResult | nullЧто вернула последняя попытка публикации.
data[].result.messagestring | nullСообщение соцсети или followers.vc.
data[].result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data[].result.social_idstring | integer | nullИдентификатор поста в соцсети.
data[].result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data[].result.thread[].social_idstring | nullId сообщения в сети.
data[].result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data[].result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data[].urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data[].metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data[].metrics.likesinteger | nullЛайки, избранное, реакции.
data[].metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data[].metrics.commentsinteger | nullОтветы и комментарии.
data[].metrics.quotesinteger | nullРепосты со своим текстом.
data[].metrics.viewsinteger | nullПросмотры или показы.
data[].metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data[].created_atinteger | nullВремя создания. Unix-секунды.
metaobject
meta.pageintegerТекущая страница, начиная с 1.
meta.per_pageintegerЭлементов на странице (по умолчанию 25, не более 100).
meta.totalintegerЭлементов на всех страницах.
linksobject
links.nextstring | nullСледующая страница; на последней — null.
links.prevstring | nullПредыдущая страница; на первой — null.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
422 validation_failed
429 rate_limited
500 server_error

POST /posts Создать пост

область posts:write тариф: apppublishing

Создаёт по одному посту на каждый канал через тот же планировщик, что и редактор. Без publish_at пост должен выйти сейчас, и его публикует очередь — никогда не внутри запроса. Если какой-либо канал отклоняет пост, ничего не сохраняется: отказ приходит в поле channels.<id>. С заголовком Idempotency-Key тот же ключ и то же тело в течение 24 часов возвращают первый ответ (Idempotent-Replayed: true).

Параметры

ИмяГдеТипОписание
Idempotency-Key header string Любая уникальная строка длиной до 255 символов. Тот же ключ с тем же телом в течение 24 часов возвращает первый успешный ответ; с другим телом — 422 idempotency_key_reused; пока первый запрос ещё выполняется — 409 idempotency_key_in_use. At most 255 characters.

Тело запроса application/json

ПолеТипОписание
channels обязательно array of string Публичные идентификаторы каналов команды; на каждый канал создаётся один пост. At least 1 items. At most 50 items.
type обязательно string One of text, link, media.
caption string | null At most 100000 characters.
thread array of string Сообщения, которые нужно опубликовать после поста: тред или комментарии — как их принимает сеть (см. thread у поста). Сети без того и другого получат только пост. At most 25 items.
thread_media object Файлы сообщений треда: номер сообщения в thread (с 0) => до 4 id файлов медиатеки. X, Bluesky и Mastodon публикуют их вместе с сообщением, остальные сети получают только его текст.
thread_delay integer Сколько минут ждать между сообщениями треда, от 0 до 60. 0 — каждое сообщение выходит сразу после предыдущего. Minimum 0. Default 0.
captions object Свой текст сети по её имени (x, telegram, facebook… — как channel.network у поста): заменяет caption в постах каналов этой сети.
signature string Подпись команды, которую нужно поставить под текстом поста и под каждым своим текстом сети: её id или default — подпись по умолчанию. Нужны подписи в тарифе команды. At most 32 characters.
channel_captions object Свой текст канала: id канала (одного из channels) => его текст. Заменяет текст поста и свой текст сети (captions) только для этого канала.
link string | null Для поста типа link. At most 2048 characters.
media array of string | null Идентификаторы из /media, в нужном порядке. At most 50 items.
media_alt object Описания файлов для программ экранного доступа: id файла => текст до 1000 знаков. Id, которых нет в media, не учитываются.
options object | null Параметры конкретной соцсети в том виде, в каком их принимает редактор.
publish_at string | integer | null ISO 8601 (2026-11-05T10:30:00+03:00, …Z) или unix-секунды. Без смещения — часовой пояс автора ключа. Время должно быть в будущем. Чтобы опубликовать сейчас, не указывайте его: пост попадёт в очередь.
channel_publish_at object Своё время канала вместо publish_at: id канала => дата и время ISO 8601 или unix-секунды, в будущем. Нужен publish_at — время остальных каналов.

Пример запроса

curl -X POST https://followers.vc/api/v1/posts \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channels":["a8d3f0c2e1b94f7d"],"type":"text","caption":"Hello from the API","thread":["string"],"thread_media":[["fil-5f1c0e2a9b"]],"thread_delay":5,"captions":{"x":"A shorter text for X"},"signature":"default","channel_captions":{"acc-5f1c0e2a9b":"Hello, our page"},"link":null,"media":[],"media_alt":{"fil-5f1c0e2a9b":"A cat asleep on a keyboard"},"options":null,"publish_at":"2026-11-05T10:30:00+03:00","channel_publish_at":{"a8d3f0c2e1b94f7d":"2026-11-05T09:30:00+03:00"}}'

Ответ 201

По одному посту на каждый канал.
{
    "data": [
        {
            "id": "kQ7x9a1b2c3d",
            "channel": {
                "id": "a8d3f0c2e1b94f7d",
                "network": "telegram",
                "name": "Acme news"
            },
            "type": "text",
            "caption": "Hello from the API",
            "thread": [
                "string"
            ],
            "thread_delay": 0,
            "link": null,
            "media": [
                {
                    "id": "fil-5f1c0e2a9b",
                    "url": "https://followers.vc/storage/files/photo.png",
                    "alt": null
                }
            ],
            "options": {},
            "status": "scheduled",
            "publish_at": 1793000000,
            "retry_at": 1793000000,
            "attempts": 0,
            "result": {
                "message": "Bad token",
                "error_class": "permanent",
                "social_id": null,
                "thread": [
                    {
                        "social_id": null,
                        "url": null,
                        "error": null
                    }
                ]
            },
            "url": null,
            "metrics": {
                "likes": 42,
                "reposts": 7,
                "comments": 5,
                "quotes": 1,
                "views": 1800,
                "updated_at": 1793003600
            },
            "created_at": 1793000000
        }
    ]
}
Поля
ПолеТипОписание
dataarray of Post
data[].idstringПубличный идентификатор поста.
data[].channelobject
data[].channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data[].channel.networkstring | null
data[].channel.namestring | null
data[].typestringtext, link или media (у более старых постов могут быть другие типы).
data[].captionstring
data[].threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data[].thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data[].linkstring | null
data[].mediaarray of objectФайлы поста по порядку.
data[].media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data[].media[].urlstring
data[].media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data[].optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data[].statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data[].publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data[].retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data[].attemptsintegerЧисло попыток публикации на данный момент.
data[].resultPostResult | nullЧто вернула последняя попытка публикации.
data[].result.messagestring | nullСообщение соцсети или followers.vc.
data[].result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data[].result.social_idstring | integer | nullИдентификатор поста в соцсети.
data[].result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data[].result.thread[].social_idstring | nullId сообщения в сети.
data[].result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data[].result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data[].urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data[].metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data[].metrics.likesinteger | nullЛайки, избранное, реакции.
data[].metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data[].metrics.commentsinteger | nullОтветы и комментарии.
data[].metrics.quotesinteger | nullРепосты со своим текстом.
data[].metrics.viewsinteger | nullПросмотры или показы.
data[].metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data[].created_atinteger | nullВремя создания. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope, post_quota
409 idempotency_key_in_use
422 validation_failed, idempotency_key_reused, not_saved
429 rate_limited
500 server_error

GET /posts/{id} Получить пост

область posts:read тариф: apppublishing

Один пост с попытками публикации.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор поста.

Пример запроса

curl -X GET https://followers.vc/api/v1/posts/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Пост.
{
    "data": {
        "id": "kQ7x9a1b2c3d",
        "channel": {
            "id": "a8d3f0c2e1b94f7d",
            "network": "telegram",
            "name": "Acme news"
        },
        "type": "text",
        "caption": "Hello from the API",
        "thread": [
            "string"
        ],
        "thread_delay": 0,
        "link": null,
        "media": [
            {
                "id": "fil-5f1c0e2a9b",
                "url": "https://followers.vc/storage/files/photo.png",
                "alt": null
            }
        ],
        "options": {},
        "status": "scheduled",
        "publish_at": 1793000000,
        "retry_at": 1793000000,
        "attempts": 0,
        "result": {
            "message": "Bad token",
            "error_class": "permanent",
            "social_id": null,
            "thread": [
                {
                    "social_id": null,
                    "url": null,
                    "error": null
                }
            ]
        },
        "url": null,
        "metrics": {
            "likes": 42,
            "reposts": 7,
            "comments": 5,
            "quotes": 1,
            "views": 1800,
            "updated_at": 1793003600
        },
        "created_at": 1793000000,
        "attempt_log": [
            {
                "attempt": 1,
                "outcome": "retry",
                "error_class": "transient",
                "message": "Rate limit",
                "at": 1793000000
            }
        ]
    }
}
Поля
ПолеТипОписание
dataPostDetail
data.idstringПубличный идентификатор поста.
data.channelobject
data.channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data.channel.networkstring | null
data.channel.namestring | null
data.typestringtext, link или media (у более старых постов могут быть другие типы).
data.captionstring
data.threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data.thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data.linkstring | null
data.mediaarray of objectФайлы поста по порядку.
data.media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data.media[].urlstring
data.media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data.optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data.statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data.publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data.retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data.attemptsintegerЧисло попыток публикации на данный момент.
data.resultPostResult | nullЧто вернула последняя попытка публикации.
data.result.messagestring | nullСообщение соцсети или followers.vc.
data.result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data.result.social_idstring | integer | nullИдентификатор поста в соцсети.
data.result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data.result.thread[].social_idstring | nullId сообщения в сети.
data.result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data.result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data.urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data.metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data.metrics.likesinteger | nullЛайки, избранное, реакции.
data.metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data.metrics.commentsinteger | nullОтветы и комментарии.
data.metrics.quotesinteger | nullРепосты со своим текстом.
data.metrics.viewsinteger | nullПросмотры или показы.
data.metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data.created_atinteger | nullВремя создания. Unix-секунды.
data.attempt_logarray of objectВсе попытки публикации, сначала старые.
data.attempt_log[].attemptinteger
data.attempt_log[].outcomestringpublished, retry, failed или reconnect.
data.attempt_log[].error_classstring | nulltransient, uncertain, auth или permanent.
data.attempt_log[].messagestring | null
data.attempt_log[].atinteger | nullВремя попытки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
429 rate_limited
500 server_error

PATCH /posts/{id} Изменить запланированный пост

область posts:write тариф: apppublishing

Переносит или редактирует запланированный пост, который сейчас не публикуется.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор поста.

Тело запроса application/json

ПолеТипОписание
caption string | null At most 100000 characters.
options object | null
publish_at string | integer | null ISO 8601 (2026-11-05T10:30:00+03:00, …Z) или unix-секунды. Время должно быть в будущем.

Пример запроса

curl -X PATCH https://followers.vc/api/v1/posts/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"caption":"New text","options":null,"publish_at":1793000000}'

Ответ 200

Изменённый пост.
{
    "data": {
        "id": "kQ7x9a1b2c3d",
        "channel": {
            "id": "a8d3f0c2e1b94f7d",
            "network": "telegram",
            "name": "Acme news"
        },
        "type": "text",
        "caption": "Hello from the API",
        "thread": [
            "string"
        ],
        "thread_delay": 0,
        "link": null,
        "media": [
            {
                "id": "fil-5f1c0e2a9b",
                "url": "https://followers.vc/storage/files/photo.png",
                "alt": null
            }
        ],
        "options": {},
        "status": "scheduled",
        "publish_at": 1793000000,
        "retry_at": 1793000000,
        "attempts": 0,
        "result": {
            "message": "Bad token",
            "error_class": "permanent",
            "social_id": null,
            "thread": [
                {
                    "social_id": null,
                    "url": null,
                    "error": null
                }
            ]
        },
        "url": null,
        "metrics": {
            "likes": 42,
            "reposts": 7,
            "comments": 5,
            "quotes": 1,
            "views": 1800,
            "updated_at": 1793003600
        },
        "created_at": 1793000000,
        "attempt_log": [
            {
                "attempt": 1,
                "outcome": "retry",
                "error_class": "transient",
                "message": "Rate limit",
                "at": 1793000000
            }
        ]
    }
}
Поля
ПолеТипОписание
dataPostDetail
data.idstringПубличный идентификатор поста.
data.channelobject
data.channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data.channel.networkstring | null
data.channel.namestring | null
data.typestringtext, link или media (у более старых постов могут быть другие типы).
data.captionstring
data.threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data.thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data.linkstring | null
data.mediaarray of objectФайлы поста по порядку.
data.media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data.media[].urlstring
data.media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data.optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data.statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data.publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data.retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data.attemptsintegerЧисло попыток публикации на данный момент.
data.resultPostResult | nullЧто вернула последняя попытка публикации.
data.result.messagestring | nullСообщение соцсети или followers.vc.
data.result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data.result.social_idstring | integer | nullИдентификатор поста в соцсети.
data.result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data.result.thread[].social_idstring | nullId сообщения в сети.
data.result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data.result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data.urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data.metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data.metrics.likesinteger | nullЛайки, избранное, реакции.
data.metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data.metrics.commentsinteger | nullОтветы и комментарии.
data.metrics.quotesinteger | nullРепосты со своим текстом.
data.metrics.viewsinteger | nullПросмотры или показы.
data.metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data.created_atinteger | nullВремя создания. Unix-секунды.
data.attempt_logarray of objectВсе попытки публикации, сначала старые.
data.attempt_log[].attemptinteger
data.attempt_log[].outcomestringpublished, retry, failed или reconnect.
data.attempt_log[].error_classstring | nulltransient, uncertain, auth или permanent.
data.attempt_log[].messagestring | null
data.attempt_log[].atinteger | nullВремя попытки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
409 not_editable
422 validation_failed
429 rate_limited
500 server_error

DELETE /posts/{id} Удалить пост

область posts:write тариф: apppublishing

Удаляет черновик, запланированный пост или пост с ошибкой, который сейчас не публикуется.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор поста.

Пример запроса

curl -X DELETE https://followers.vc/api/v1/posts/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 204

Удалено.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
409 not_deletable
429 rate_limited
500 server_error

POST /posts/{id}/retry Повторить публикацию поста с ошибкой

область posts:write тариф: apppublishing

Возвращает пост с ошибкой в очередь.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор поста.

Пример запроса

curl -X POST https://followers.vc/api/v1/posts/{id}/retry \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Пост, снова ожидающий публикации.
{
    "data": {
        "id": "kQ7x9a1b2c3d",
        "channel": {
            "id": "a8d3f0c2e1b94f7d",
            "network": "telegram",
            "name": "Acme news"
        },
        "type": "text",
        "caption": "Hello from the API",
        "thread": [
            "string"
        ],
        "thread_delay": 0,
        "link": null,
        "media": [
            {
                "id": "fil-5f1c0e2a9b",
                "url": "https://followers.vc/storage/files/photo.png",
                "alt": null
            }
        ],
        "options": {},
        "status": "scheduled",
        "publish_at": 1793000000,
        "retry_at": 1793000000,
        "attempts": 0,
        "result": {
            "message": "Bad token",
            "error_class": "permanent",
            "social_id": null,
            "thread": [
                {
                    "social_id": null,
                    "url": null,
                    "error": null
                }
            ]
        },
        "url": null,
        "metrics": {
            "likes": 42,
            "reposts": 7,
            "comments": 5,
            "quotes": 1,
            "views": 1800,
            "updated_at": 1793003600
        },
        "created_at": 1793000000,
        "attempt_log": [
            {
                "attempt": 1,
                "outcome": "retry",
                "error_class": "transient",
                "message": "Rate limit",
                "at": 1793000000
            }
        ]
    }
}
Поля
ПолеТипОписание
dataPostDetail
data.idstringПубличный идентификатор поста.
data.channelobject
data.channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data.channel.networkstring | null
data.channel.namestring | null
data.typestringtext, link или media (у более старых постов могут быть другие типы).
data.captionstring
data.threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data.thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data.linkstring | null
data.mediaarray of objectФайлы поста по порядку.
data.media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data.media[].urlstring
data.media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data.optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data.statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data.publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data.retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data.attemptsintegerЧисло попыток публикации на данный момент.
data.resultPostResult | nullЧто вернула последняя попытка публикации.
data.result.messagestring | nullСообщение соцсети или followers.vc.
data.result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data.result.social_idstring | integer | nullИдентификатор поста в соцсети.
data.result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data.result.thread[].social_idstring | nullId сообщения в сети.
data.result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data.result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data.urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data.metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data.metrics.likesinteger | nullЛайки, избранное, реакции.
data.metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data.metrics.commentsinteger | nullОтветы и комментарии.
data.metrics.quotesinteger | nullРепосты со своим текстом.
data.metrics.viewsinteger | nullПросмотры или показы.
data.metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data.created_atinteger | nullВремя создания. Unix-секунды.
data.attempt_logarray of objectВсе попытки публикации, сначала старые.
data.attempt_log[].attemptinteger
data.attempt_log[].outcomestringpublished, retry, failed или reconnect.
data.attempt_log[].error_classstring | nulltransient, uncertain, auth или permanent.
data.attempt_log[].messagestring | null
data.attempt_log[].atinteger | nullВремя попытки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
409 not_retryable
429 rate_limited
500 server_error

POST /posts/{id}/link Привязать вышедший пост к записи в сети

область posts:write тариф: apppublishing

Для неудачного поста с ошибкой uncertain (запрос мог пройти, поэтому пост не повторяется): отмечает его опубликованным со ссылкой на запись в сети. Ссылка должна быть https, а для сети с известными адресами — на одном из них; id записи в сети берётся из ссылки, где она его содержит (result.social_id). В журнал попыток добавляется строка published, срабатывает вебхук post.published.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор поста.

Тело запроса application/json

ПолеТипОписание
url обязательно string (uri) Ссылка https на запись в сети. At most 2000 characters.

Пример запроса

curl -X POST https://followers.vc/api/v1/posts/{id}/link \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://x.com/someone/status/1849000000000000001"}'

Ответ 200

Пост, опубликованный со ссылкой.
{
    "data": {
        "id": "kQ7x9a1b2c3d",
        "channel": {
            "id": "a8d3f0c2e1b94f7d",
            "network": "telegram",
            "name": "Acme news"
        },
        "type": "text",
        "caption": "Hello from the API",
        "thread": [
            "string"
        ],
        "thread_delay": 0,
        "link": null,
        "media": [
            {
                "id": "fil-5f1c0e2a9b",
                "url": "https://followers.vc/storage/files/photo.png",
                "alt": null
            }
        ],
        "options": {},
        "status": "scheduled",
        "publish_at": 1793000000,
        "retry_at": 1793000000,
        "attempts": 0,
        "result": {
            "message": "Bad token",
            "error_class": "permanent",
            "social_id": null,
            "thread": [
                {
                    "social_id": null,
                    "url": null,
                    "error": null
                }
            ]
        },
        "url": null,
        "metrics": {
            "likes": 42,
            "reposts": 7,
            "comments": 5,
            "quotes": 1,
            "views": 1800,
            "updated_at": 1793003600
        },
        "created_at": 1793000000,
        "attempt_log": [
            {
                "attempt": 1,
                "outcome": "retry",
                "error_class": "transient",
                "message": "Rate limit",
                "at": 1793000000
            }
        ]
    }
}
Поля
ПолеТипОписание
dataPostDetail
data.idstringПубличный идентификатор поста.
data.channelobject
data.channel.idstring | nullПубличный идентификатор канала; null, если канал удалён.
data.channel.networkstring | null
data.channel.namestring | null
data.typestringtext, link или media (у более старых постов могут быть другие типы).
data.captionstring
data.threadarray of stringСообщения, вышедшие после поста: тред из ответов там, где у сети есть треды (X, Bluesky, Mastodon, Threads, Telegram, Discord, Slack), комментарии под постом там, где есть комментарии (Facebook, Instagram, Reddit, ВКонтакте). Пусто, если их нет.
data.thread_delayintegerМинуты между сообщениями треда; 0 — сразу.
data.linkstring | null
data.mediaarray of objectФайлы поста по порядку.
data.media[].idstring | nullИдентификатор в медиатеке; null, если файла там нет.
data.media[].urlstring
data.media[].altstring | nullОписание файла для программ экранного доступа; передаётся в сети, которые его принимают (Bluesky, Mastodon, X, Pinterest). null, если описания нет.
data.optionsobjectПараметры конкретной соцсети в том виде, в каком они были переданы.
data.statusstringreconnect_wait — пост запланирован, но канал нужно переподключить. unknown — статус, который API не называет. One of draft, scheduled, reconnect_wait, published, failed, unknown.
data.publish_atinteger | nullКогда пост должен выйти (или должен был выйти); для черновика — null. Unix-секунды.
data.retry_atinteger | nullВременная ошибка: очередь повторит попытку в это время. Unix-секунды.
data.attemptsintegerЧисло попыток публикации на данный момент.
data.resultPostResult | nullЧто вернула последняя попытка публикации.
data.result.messagestring | nullСообщение соцсети или followers.vc.
data.result.error_classstring | nulltransient, uncertain (пост мог быть опубликован), auth или permanent; null — ошибки нет.
data.result.social_idstring | integer | nullИдентификатор поста в соцсети.
data.result.threadarray of objectЧто вышло с каждым сообщением треда, по порядку; публикация останавливается на первом отказе.
data.result.thread[].social_idstring | nullId сообщения в сети.
data.result.thread[].urlstring | nullАдрес сообщения, если сеть его даёт.
data.result.thread[].errorstring | nullПочему сеть отклонила сообщение или почему сеть не принимает тред.
data.urlstring | nullАдрес опубликованного поста, если соцсеть его возвращает.
data.metricsPostMetrics | nullСчётчики опубликованного поста. Счётчик, которого у сети нет, равен null: Bluesky, Mastodon и Reddit не считают просмотры, цитаты есть только у X, Bluesky, Mastodon и Threads.
data.metrics.likesinteger | nullЛайки, избранное, реакции.
data.metrics.repostsinteger | nullРепосты, ретвиты, бусты.
data.metrics.commentsinteger | nullОтветы и комментарии.
data.metrics.quotesinteger | nullРепосты со своим текстом.
data.metrics.viewsinteger | nullПросмотры или показы.
data.metrics.updated_atintegerКогда счётчики прочитаны. Unix-секунды.
data.created_atinteger | nullВремя создания. Unix-секунды.
data.attempt_logarray of objectВсе попытки публикации, сначала старые.
data.attempt_log[].attemptinteger
data.attempt_log[].outcomestringpublished, retry, failed или reconnect.
data.attempt_log[].error_classstring | nulltransient, uncertain, auth или permanent.
data.attempt_log[].messagestring | null
data.attempt_log[].atinteger | nullВремя попытки. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
404 not_found
409 not_linkable
422 validation_failed
429 rate_limited
500 server_error

Notifications

Уведомления автора ключа.

GET /notifications Список уведомлений

область notifications:read

Уведомления автора ключа — те же, что в колокольчике кабинета, — сначала новые.

Параметры

ИмяГдеТипОписание
unread query boolean | integer 1 — только непрочитанные. One of true, false, 0, 1.
page query integer Страница, начиная с 1. Minimum 1. Default 1.
per_page query integer Число элементов на странице; значение больше 100 урезается до 100. Minimum 1. Default 25.

Пример запроса

curl -X GET https://followers.vc/api/v1/notifications \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Страница уведомлений.
{
    "data": [
        {
            "id": "n-7d1e2f",
            "message": "Your post on Acme news has been published.",
            "url": null,
            "source": "auto",
            "type": "news",
            "read": false,
            "created_at": 1793000000
        }
    ],
    "meta": {
        "page": 1,
        "per_page": 25,
        "total": 40
    },
    "links": {
        "next": "https://followers.vc/api/v1/posts?per_page=25&page=2",
        "prev": null
    }
}
Поля
ПолеТипОписание
dataarray of Notification
data[].idstringПубличный идентификатор уведомления.
data[].messagestring | null
data[].urlstring | nullКуда ведёт уведомление.
data[].sourcestringauto — отправлено followers.vc, manual — администраторами. One of auto, manual.
data[].typestring | null
data[].readboolean
data[].created_atinteger | nullВремя уведомления. Unix-секунды.
metaobject
meta.pageintegerТекущая страница, начиная с 1.
meta.per_pageintegerЭлементов на странице (по умолчанию 25, не более 100).
meta.totalintegerЭлементов на всех страницах.
linksobject
links.nextstring | nullСледующая страница; на последней — null.
links.prevstring | nullПредыдущая страница; на первой — null.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, insufficient_scope
422 validation_failed
429 rate_limited
500 server_error

GET /notifications/unread-count Количество непрочитанных уведомлений

область notifications:read

Сколько уведомлений автора ключа не прочитано.

Пример запроса

curl -X GET https://followers.vc/api/v1/notifications/unread-count \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Количество.
{
    "data": {
        "unread": 3
    }
}
Поля
ПолеТипОписание
dataUnreadCount
data.unreadinteger

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, insufficient_scope
429 rate_limited
500 server_error

POST /notifications/{id}/read Отметить уведомление прочитанным

область notifications:write

Можно повторять: уже прочитанное уведомление тоже отвечает 200.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор уведомления.

Пример запроса

curl -X POST https://followers.vc/api/v1/notifications/{id}/read \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Уведомление.
{
    "data": {
        "id": "n-7d1e2f",
        "message": "Your post on Acme news has been published.",
        "url": null,
        "source": "auto",
        "type": "news",
        "read": false,
        "created_at": 1793000000
    }
}
Поля
ПолеТипОписание
dataNotification
data.idstringПубличный идентификатор уведомления.
data.messagestring | null
data.urlstring | nullКуда ведёт уведомление.
data.sourcestringauto — отправлено followers.vc, manual — администраторами. One of auto, manual.
data.typestring | null
data.readboolean
data.created_atinteger | nullВремя уведомления. Unix-секунды.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, insufficient_scope
404 not_found
429 rate_limited
500 server_error

POST /notifications/read-all Отметить все уведомления прочитанными

область notifications:write

Отмечает прочитанными все уведомления автора ключа.

Пример запроса

curl -X POST https://followers.vc/api/v1/notifications/read-all \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Непрочитанных не осталось.
{
    "data": {
        "unread": 3
    }
}
Поля
ПолеТипОписание
dataUnreadCount
data.unreadinteger

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, insufficient_scope
429 rate_limited
500 server_error

Analytics

Отчёты по каналам из их соцсетей.

GET /analytics Каналы с аналитикой

область analytics:read тариф: appanalytics

Каналы команды, для которых есть отчёт, сгруппированные по соцсетям. Соцсети, не входящие в тариф, пропускаются.

Пример запроса

curl -X GET https://followers.vc/api/v1/analytics \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Соцсети с их каналами.
{
    "data": [
        {
            "network": "facebook",
            "name": "Facebook",
            "channels": [
                {
                    "id": "a8d3f0c2e1b94f7d",
                    "name": "Acme page",
                    "username": "acme",
                    "avatar": null
                }
            ]
        }
    ]
}
Поля
ПолеТипОписание
dataarray of AnalyticsNetwork
data[].networkstring
data[].namestring
data[].channelsarray of object
data[].channels[].idstringПубличный идентификатор канала.
data[].channels[].namestring | null
data[].channels[].usernamestring | null
data[].channels[].avatarstring | null

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope
429 rate_limited
500 server_error

GET /analytics/{id} Отчёт по каналу

область analytics:read тариф: appanalytics

Отчёт по одному каналу из того же сервиса соцсети, что и в кабинете. Соцсеть может опрашиваться прямо во время запроса, поэтому он может выполняться долго.

Параметры

ИмяГдеТипОписание
id обязательно path string Публичный идентификатор канала.
from query string ISO 8601 или unix-секунды; дата без времени охватывает весь день. По умолчанию — за 30 дней до to. At most 64 characters.
to query string ISO 8601 или unix-секунды. По умолчанию — текущий момент. Диапазон — не более 365 дней. At most 64 characters.

Пример запроса

curl -X GET https://followers.vc/api/v1/analytics/{id} \
  -H "Authorization: Bearer $FOLLOWERS_API_KEY"

Ответ 200

Отчёт.
{
    "data": {
        "followers": 120,
        "chart": [
            1,
            2,
            3
        ]
    },
    "meta": {
        "channel": "a8d3f0c2e1b94f7d",
        "from": "2026-09-16T00:00:00+00:00",
        "to": "2026-10-16T09:30:00+00:00"
    }
}
Поля
ПолеТипОписание
dataobject | arrayОтчёт в том виде, в каком его строит сервис соцсети (те же цифры, что показывает кабинет). Поля зависят от соцсети.
metaobject
meta.channelstringПубличный идентификатор канала.
meta.fromstring (date-time)Начало диапазона, ISO 8601.
meta.tostring (date-time)Конец диапазона, ISO 8601.

Ошибки

СтатусКоды
401 unauthenticated
403 api_not_in_plan, plan_expired, module_not_in_plan, insufficient_scope, analytics_not_in_plan
404 not_found, analytics_unavailable
422 validation_failed
429 rate_limited
500 server_error
502 provider_error

Вебхуки

followers.vc отправляет подписанный POST с телом JSON на ваш адрес https, когда что-то происходит с вашими постами и каналами. Вебхуки настраиваются на странице «Вебхуки» в кабинете.

СОБЫТИЕ post.scheduled Пост запланирован

Пост запланирован на определённое время (одно событие на канал и время).

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "post.scheduled",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "post": {
            "id": "kQ7x9a1",
            "status": "scheduled",
            "type": "media",
            "time_post": "2026-10-16T09:30:00Z",
            "url": null
        },
        "channel": {
            "id": "a8d3f0c2",
            "network": "telegram",
            "name": "Example"
        }
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"post.scheduled"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.postWebhookPost
data.post.idstringПубличный идентификатор поста.
data.post.statusstringOne of scheduled, published, failed.
data.post.typestring | null
data.post.time_poststring | null (date-time)Когда пост должен выйти, ISO 8601 UTC.
data.post.urlstring | nullЗаполнено у опубликованного поста, если соцсеть возвращает ссылку.
data.channelWebhookChannel
data.channel.idstring | nullПубличный идентификатор канала.
data.channel.networkstring | null
data.channel.namestring | null
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ post.published Пост опубликован

Пост опубликован в соцсети.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "post.published",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "post": {
            "id": "kQ7x9a1",
            "status": "published",
            "type": "media",
            "time_post": "2026-10-16T09:30:00Z",
            "url": "https://t.me/example/123"
        },
        "channel": {
            "id": "a8d3f0c2",
            "network": "telegram",
            "name": "Example"
        }
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"post.published"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.postWebhookPost
data.post.idstringПубличный идентификатор поста.
data.post.statusstringOne of scheduled, published, failed.
data.post.typestring | null
data.post.time_poststring | null (date-time)Когда пост должен выйти, ISO 8601 UTC.
data.post.urlstring | nullЗаполнено у опубликованного поста, если соцсеть возвращает ссылку.
data.channelWebhookChannel
data.channel.idstring | nullПубличный идентификатор канала.
data.channel.networkstring | null
data.channel.namestring | null
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ post.failed Публикация поста окончательно не удалась

Публикация поста окончательно не удалась: автоматических попыток больше не будет.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "post.failed",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "post": {
            "id": "kQ7x9a1",
            "status": "failed",
            "type": "media",
            "time_post": "2026-10-16T09:30:00Z",
            "url": null
        },
        "channel": {
            "id": "a8d3f0c2",
            "network": "telegram",
            "name": "Example"
        },
        "error": {
            "class": "permanent",
            "message": "The token was revoked."
        }
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"post.failed"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.postWebhookPost
data.post.idstringПубличный идентификатор поста.
data.post.statusstringOne of scheduled, published, failed.
data.post.typestring | null
data.post.time_poststring | null (date-time)Когда пост должен выйти, ISO 8601 UTC.
data.post.urlstring | nullЗаполнено у опубликованного поста, если соцсеть возвращает ссылку.
data.channelWebhookChannel
data.channel.idstring | nullПубличный идентификатор канала.
data.channel.networkstring | null
data.channel.namestring | null
data.errorobject
data.error.classstring | nullpermanent, transient, uncertain (пост мог быть опубликован) или auth. One of permanent, transient, uncertain, auth.
data.error.messagestring | nullНе более 500 символов.
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ channel.disconnected Канал нужно переподключить

Канал потерял доступ и требует переподключения; его посты ждут.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "channel.disconnected",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "channel": {
            "id": "a8d3f0c2",
            "network": "telegram",
            "name": "Example"
        },
        "reason": "The token was revoked."
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"channel.disconnected"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.channelWebhookChannel
data.channel.idstring | nullПубличный идентификатор канала.
data.channel.networkstring | null
data.channel.namestring | null
data.reasonstringПочему канал нужно переподключить; не более 500 символов.
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ approval.approved Пост одобрен

Проверяющий одобрил посты на публичной странице согласования.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "approval.approved",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "approval": {
            "id": "Zp4r2Lx8",
            "status": "approved",
            "decided_by": "Anna",
            "decided_at": "2026-10-16T09:30:00Z",
            "comment": "Looks good."
        },
        "posts": [
            {
                "post": {
                    "id": "kQ7x9a1",
                    "status": "scheduled",
                    "type": "media",
                    "time_post": "2026-10-17T09:00:00Z",
                    "url": null
                },
                "channel": {
                    "id": "a8d3f0c2",
                    "network": "telegram",
                    "name": "Example"
                }
            }
        ]
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"approval.approved"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.approvalobjectРешение: одобрено.
data.approval.idstringИдентификатор запроса на согласование: одинаков для всех решений по нему.
data.approval.status"approved"
data.approval.decided_bystring | nullИмя, которое проверяющий указал на странице согласования.
data.approval.decided_atstring | null (date-time)Время решения, ISO 8601 UTC.
data.approval.commentstring | nullЧто проверяющий написал вместе с решением; не более 500 символов.
data.postsarray of objectПосты запроса на согласование, по одному на канал.
data.posts[].postWebhookPost
data.posts[].post.idstringПубличный идентификатор поста.
data.posts[].post.statusstringOne of scheduled, published, failed.
data.posts[].post.typestring | null
data.posts[].post.time_poststring | null (date-time)Когда пост должен выйти, ISO 8601 UTC.
data.posts[].post.urlstring | nullЗаполнено у опубликованного поста, если соцсеть возвращает ссылку.
data.posts[].channelWebhookChannel
data.posts[].channel.idstring | nullПубличный идентификатор канала.
data.posts[].channel.networkstring | null
data.posts[].channel.namestring | null
data.posts[].errorobjectТолько для поста, публикация которого не удалась.
data.posts[].error.classstring | nullpermanent, transient, uncertain (пост мог быть опубликован) или auth. One of permanent, transient, uncertain, auth.
data.posts[].error.messagestring | nullНе более 500 символов.
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ approval.changes_requested Запрошены правки поста

Проверяющий запросил правки на публичной странице согласования; посты ждут нового согласования.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "approval.changes_requested",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "approval": {
            "id": "Zp4r2Lx8",
            "status": "changes_requested",
            "decided_by": "Anna",
            "decided_at": "2026-10-16T09:30:00Z",
            "comment": "Please shorten the text."
        },
        "posts": [
            {
                "post": {
                    "id": "kQ7x9a1",
                    "status": "scheduled",
                    "type": "media",
                    "time_post": "2026-10-17T09:00:00Z",
                    "url": null
                },
                "channel": {
                    "id": "a8d3f0c2",
                    "network": "telegram",
                    "name": "Example"
                }
            }
        ]
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"approval.changes_requested"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.approvalobjectРешение: запрошены правки.
data.approval.idstringИдентификатор запроса на согласование: одинаков для всех решений по нему.
data.approval.status"changes_requested"
data.approval.decided_bystring | nullИмя, которое проверяющий указал на странице согласования.
data.approval.decided_atstring | null (date-time)Время решения, ISO 8601 UTC.
data.approval.commentstring | nullЧто проверяющий написал вместе с решением; не более 500 символов.
data.postsarray of objectПосты запроса на согласование, по одному на канал.
data.posts[].postWebhookPost
data.posts[].post.idstringПубличный идентификатор поста.
data.posts[].post.statusstringOne of scheduled, published, failed.
data.posts[].post.typestring | null
data.posts[].post.time_poststring | null (date-time)Когда пост должен выйти, ISO 8601 UTC.
data.posts[].post.urlstring | nullЗаполнено у опубликованного поста, если соцсеть возвращает ссылку.
data.posts[].channelWebhookChannel
data.posts[].channel.idstring | nullПубличный идентификатор канала.
data.posts[].channel.networkstring | null
data.posts[].channel.namestring | null
data.posts[].errorobjectТолько для поста, публикация которого не удалась.
data.posts[].error.classstring | nullpermanent, transient, uncertain (пост мог быть опубликован) или auth. One of permanent, transient, uncertain, auth.
data.posts[].error.messagestring | nullНе более 500 символов.
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.

СОБЫТИЕ ping Тестовая доставка

Отправляется только кнопкой Send test на странице Webhooks.

Тело

{
    "id": "5b0c2a4e-1f0e-4c55-9a43-2d8c7e1b6f10",
    "event": "ping",
    "created_at": "2026-10-16T09:30:00Z",
    "data": {
        "webhook": {
            "id": "Xh2k9",
            "events": [
                "post.published"
            ]
        },
        "message": "Test delivery from followers.vc"
    }
}
Поля
ПолеТипОписание
idstring (uuid)Идентификатор события: одинаковый при повторах и ручных повторных отправках — используйте его, чтобы пропускать дубликаты.
event"ping"
created_atstring (date-time)Время события, ISO 8601 UTC.
dataobject
data.webhookobject
data.webhook.idstring
data.webhook.eventsarray of string
data.messagestring
Заголовки и ответы
ЗаголовокОписание
X-Followers-EventСобытие, например post.published.
X-Followers-DeliveryИдентификатор этой доставки (при ручной повторной отправке новый).
X-Followers-TimestampUnix-время подписи запроса.
X-Followers-Signaturet=<timestamp>,v1=<signature>: HMAC-SHA256 в шестнадцатеричном виде от <timestamp>.<raw body> с секретом вебхука (whsec_…). Отклоняйте запрос, если t отличается от ваших часов более чем на 5 минут.
User-Agentfollowers.vc-Webhooks/1.0.
Ваш ответЧто происходит
2XXЛюбой ответ 2xx в течение 10 секунд — доставлено. Тело ответа не используется.
defaultНет ответа, тайм-аут, 429 или 5xx — повтор через 1 минуту, 5 минут, 30 минут, 2 часа и 12 часов. Любой другой ответ 4xx или 3xx — сразу неудача. После 20 неудачных запросов подряд вебхук отключается.