Smmly

API работает · ключ выдаётся в кабинете за минуту

API для постинга в телеграм, ВКонтакте и MAX

Один POST — и пост уходит сразу во все ваши каналы: телеграм, ВКонтакте, MAX, X. Очередь, повторы и ошибки площадок берём на себя; вы шлёте JSON из крона, бэкенда или скрипта на ноутбуке.

Нужен аккаунт с подключёнными каналами · 7 дней доступа за 10 ₽

Первый пост за три шага

  1. Возьмите ключ

    Ключи доступа → «Создать ключ». Он показывается один раз и начинается с smk_ — скопируйте сразу. У нас в базе лежит только его хеш, восстановить строку нельзя: потеряли — удалите и выпустите новый.

  2. Отправьте пост

    Подставьте свой ключ и ярлык канала. Ярлыки — то, чем каналы адресуются: посмотреть их можно запросом GET /api/v1/channelsили в кабинете.

    Публикация в каналы с ярлыком «новости»
    curl -X POST https://smmly.ru/api/v1/posts \
      -H "Authorization: Bearer smk_ВАШ_КЛЮЧ" \
      -H "Content-Type: application/json" \
      -d '{
        "tags": ["новости"],
        "content": "Привезли новую обжарку — ждём с утра.",
        "source": "moj-kron",
        "externalKey": "obzharka-2026-09-05"
      }'
  3. Заберите ответ

    201 и groupId — пост принят и уйдёт в течение минуты. По строке на каждый канал, куда он поставлен. Дальше судьбу поста смотрите по этому же groupId.

    Ответ
    HTTP/1.1 201 Created
    
    {
      "groupId": "6f1c2f3a-8d21-4a77-9d0e-2b7c1f0a5e44",
      "posts": [
        { "id": 812, "channelId": 17, "provider": "telegram", "name": "Кофейня на Мойке", "state": "queued" },
        { "id": 813, "channelId": 18, "provider": "vk",       "name": "Кофейня на Мойке", "state": "queued" }
      ]
    }

Четыре ручки, и всё

Подключение каналов, оплата, выдача ключей и токены площадок наружу не выставлены намеренно: ключ даёт право вести каналы, а не управлять аккаунтом.

GET/api/v1/channels

Куда можно постить

С этого начинает любой клиент: id, площадка, имя канала и ярлыки. Токенов площадок и настроек подключения ручка не отдаёт вовсе. Параметр ?all=1 добавит выключенные каналы — чтобы понять, почему канал перестал получать посты.

Запрос
curl https://smmly.ru/api/v1/channels \
  -H "Authorization: Bearer smk_ВАШ_КЛЮЧ"
Ответ
{
  "channels": [
    { "id": 17, "provider": "telegram", "name": "Кофейня на Мойке", "tags": ["новости", "акции"], "disabled": false, "needsReauth": false },
    { "id": 18, "provider": "vk",       "name": "Кофейня на Мойке", "tags": ["новости"],           "disabled": false, "needsReauth": false }
  ]
}

POST/api/v1/posts

Положить пост в очередь

Один вызов — один замысел сразу в несколько каналов. Куда: ярлыком (tags), площадкой целиком (providers) или списком id (channelIds); нужно хотя бы одно. Пост встаёт в ту же очередь, что и написанный руками в кабинете, и его разносит тот же отправщик.

Тело запроса целиком
{
  "tags": ["новости"],              // или "providers": ["telegram"], или "channelIds": [17, 18]
  "content": "текст на все площадки",
  // либо по площадкам: {"default": "…", "telegram": "<b>…</b>", "vk": "…", "max": "…", "x": "…"}
  "format": "html",                 // plain (по умолчанию) или html
  "mediaUrls": ["https://site.ru/card.png"],
  "media": [{"type": "image", "rel": "…"}],   // ответ /api/v1/media, как есть
  "buttons": [
    {"text": "Записаться", "url": "https://site.ru/zapis/"}
  ],
  "firstComment": "Подробности: …",
  "partial": true,                  // отказ одного канала не валит остальные; отвергнутые придут в skipped
  "publishAt": "2026-09-06T09:00:00+03:00",   // нет → уходит сейчас
  "state": "queued",                // draft — положить в кабинет, но не отправлять
  "source": "moj-kron",             // пара source + externalKey = защита от повтора
  "externalKey": "obzharka-2026-09-05",
  "payload": {}                     // что угодно, ляжет рядом с событием источника
}

GET/api/v1/posts?group=…

Что стало с постом

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

Запрос
curl "https://smmly.ru/api/v1/posts?group=6f1c2f3a-8d21-4a77-9d0e-2b7c1f0a5e44" \
  -H "Authorization: Bearer smk_ВАШ_КЛЮЧ"
Ответ
{
  "groupId": "6f1c2f3a-8d21-4a77-9d0e-2b7c1f0a5e44",
  "posts": [
    {
      "id": 812, "channelId": 17, "provider": "telegram", "name": "Кофейня на Мойке",
      "state": "published",
      "publishAt": "2026-09-05T06:00:00.000Z",
      "publishedAt": "2026-09-05T06:00:11.402Z",
      "externalUrl": "https://t.me/mycoffee/1481",
      "error": null,
      "attempts": 1
    }
  ]
}

POST/api/v1/media

Залить вложение

Нужна, когда публичной ссылки на файл нет: карточка родилась в памяти скрипта, скриншот лежит на диске. У кого ссылка есть — шлёт её прямо в mediaUrls поста, тогда запрос всего один. Принимает multipart (поле file) или голое тело с Content-Type файла и именем в X-File-Name. Блок media из ответа кладётся в пост как есть; url в ответе — наш роут отдачи за авторизацией, публичного адреса у файла не появляется и площадке он не нужен: туда файл уходит содержимым.

Запрос
curl -X POST https://smmly.ru/api/v1/media \
  -H "Authorization: Bearer smk_ВАШ_КЛЮЧ" \
  -F "file=@card.png"
Ответ
{
  "media": { "type": "image", "rel": "42/6f1c2f3a-….png", "name": "card.png", "size": 184320 },
  "url": "/api/media/42/6f1c2f3a-….png"
}

Поле title в контракте есть, но сегодня оно ни на что не влияет: ни одна из четырёх подключаемых площадок отдельного заголовка у поста не различает. Всё решает текст.

Ради этого API и затевался

Крон перезапустился — пост не уйдёт дважды

Передайте пару source +externalKey: имя вашего процесса и ваш собственный идентификатор события — номер выплаты, id заказа, дату слота. Пара уникальна на всю базу, и повторный запрос с ней не отправляет ничего: возвращается200 с duplicate: trueи тем же groupId, что и в первый раз. Работает это навсегда, а не несколько минут: та же выплата не уедет второй раз и через неделю.

Клиенту от этого проще некуда: не нужно помнить, что уже отправлено, не нужна своя таблица «постил / не постил». Упал посреди прогона, перезапустился, прогнал заново — лишнего не будет.

Второй вызов с той же парой
# тот же source + externalKey во второй раз
HTTP/1.1 200 OK

{
  "duplicate": true,
  "groupId": "6f1c2f3a-8d21-4a77-9d0e-2b7c1f0a5e44",
  "posts": [{ "id": 812, "channelId": 17, "state": "published" }]
}

Оба поля задаются только вместе: одно без другого — 400.source обрезается до 64 знаков,externalKey — до 255.

Ключа не передали — защита всё равно есть, но другая и короткая: одинаковый запрос в те же каналы в пределах пяти минут считается повтором обрыва и второго поста не создаёт. Дальше пяти минут тот же текст уедет ещё раз — это уже осознанный повтор, и запрещать его мы не вправе. Крону нужна вечность, поэтому крону нужен ключ.

Что доедет до каждой площадки

Один замысел — четыре разных набора правил. Всё проверяемое мы проверяем при постановке в очередь, а не в момент отправки: узнать, что текст на триста знаков длиннее лимита, надо ДО того, как половина каналов уже опубликовалась.

ЧтоТелеграмВКонтактеMAXX
Длина текста4096 знаков4096 знаков4000 знаков280 знаков
С вложением1024 в подписи — дальше текст уходит вторым сообщением, пост не отбиваетсяте же 4096 — вложение лимит не режетте же 4000те же 280
Вложениядо 10 картинок, видео и файлов (альбом); файл нельзя смешивать с картинкойдо 10 картинок; видео и файлы пока не грузимдо 12 вложений на сообщение, картинки и видео; файлы пока не грузимдо 4 картинок; видео пока не грузим
Разметка HTMLданет — теги снимаются, уходит чистый текстданет — теги снимаются
Кнопки-ссылкидо 8, по одной в ряднетнетнет
Второе сообщениеданетнетда — уходит ответом на свой же пост
Правка опубликованногода, пока посту не больше двух сутокнет — только снять и опубликовать зановонетнет
Ссылка на постесть у публичного канала, у приватного — нетесть всегдаможет не бытьесть всегда
  • Разметку и кнопки, которых площадка не умеет, мы не отбиваем, а тихо не шлём: пост во ВКонтакте уедет без тегов, в MAX — без кнопки. Отбить публикацию из-за кнопки было бы хуже, чем опубликовать без неё.
  • Единственное исключение — кнопки на альбоме: телеграм не принимает клавиатуру к группе вложений вовсе, и такой пост мы отбиваем (400) вместо тихой потери кнопок, ради которых его и собирали.
  • Вложений в одном посте — не больше десяти, считая и media, и mediaUrls. Ссылки мы скачиваем сами, по разу на замысел, а не на канал.

На этом API живёт наш собственный крон

Внешний вход затевался не под витрину: в него ходит крон Инвестминта — соседнего нашего проекта. Объявили дивиденды, скрипт нарисовал карточку и положил пост в очередь; дальше он живёт как любой другой, тем же отправщиком, с теми же повторами и ошибками.

Именно из-за него появилась пара source +externalKey: перезапуск тика без неё публиковал бы одну и ту же выплату дважды.

  1. 1 Крон увидел новую выплату
  2. 2 Собрал карточку и текст
  3. 3 Положил пост с externalKey вида SBER:2026:12M
  4. 4 Пост ушёл во все каналы с нужным ярлыком, одним замыслом
  5. 5 Тик повторился — вернулся duplicate, ничего не отправлено

API или MCP

Это не два продукта, а два входа в одну очередь. Ключ один и тот же, каналы те же, посты видны в одном календаре. Разница — кто принимает решение опубликовать.

Вы читаете эту страницу

HTTP API — когда постит программа

  • Крон, бэкенд, скрипт, кнопка в вашей админке
  • Защита от повтора по source + externalKey
  • Предсказуемые коды ответа и разбор ошибок в коде
  • Черновики: state: draft — положить, но не отправлять

Соседний вход

MCP — когда постит агент словами

  • «Запости это в новости» прямо в разговоре с Claude или другим агентом
  • Девять инструментов: показать каналы, опубликовать, поправить, снять
  • Правка и снятие поста — то, чего у HTTP-ручек пока нет
  • Свой ключ повтора тоже есть — одним полем idempotencyKey

Отказы и что с ними делать

  • 400

    Тело не прошло проверку

    «Нужен content…», «Укажите, куда постить…», «Под этот отбор не нашлось ни одного живого канала». Отдельный случай — «Пост не проходит по площадкам» со списком в issues: там по строке на канал, что именно длиннее лимита или несовместимо.

  • 401

    Ключа нет или он мёртв

    Проверьте заголовок: Authorization: Bearer smk_… Удалённый в кабинете ключ перестаёт работать сразу, и «нет такого» от «отозван» снаружи не отличается намеренно.

  • 402

    Нет действующей подписки

    Очередь закрыта: код no_subscription. Черновик (state: draft) при этом принимается — терять уже написанный текст из-за просроченной карты незачем.

  • 403

    Почта аккаунта не подтверждена

    Тот же гейт, что и в кабинете. Ключ его не обходит — иначе он был бы дыркой в обход самой проверки.

  • 404

    Замысел не найден

    GET по чужому или несуществующему groupId. Формат обязан быть uuid.

  • 413

    Файл больше предела

    Картинка — до 10 МБ, видео и pdf — до 50 МБ.

  • 429

    Слишком часто

    Больше 60 запросов в минуту на ключ. Отказ держится до конца минуты, повторять стоит с паузой.

Частые вопросы

Чем API отличается от MCP и что выбрать?

API — для кода: крон, бэкенд, скрипт. Вы сами формируете JSON, сами решаете, когда его отправить, и разбираете коды ответа. MCP — для разговора: вы говорите агенту «запости это в новости», решение принимает он. Ключ у обоих один и тот же, каналы и очередь общие, защита от повтора тоже есть у обоих. Если постит программа по расписанию — берите API; если человек словами — MCP.

Сколько это стоит и считаете ли вы публикации?

Публикации не считаем вовсе — ни через API, ни из кабинета. Тариф считается КАНАЛАМИ: сколько площадок и сообществ подключено, столько и платите. API входит в любой тариф отдельной строкой в счёте не идёт. Начать можно с 7 дней полного доступа за 10 ₽.

Что будет, если площадка откажет?

Проверяемое мы проверяем ДО записи: длина, вложения, разметка, кнопки — по всем выбранным каналам сразу. Не прошло по одной площадке — не создаём ничего и отвечаем 400 со списком в issues, чтобы половина каналов не оказалась опубликована. А вот отказ самой площадки в момент отправки — уже другое дело: пост встаёт в состояние error с текстом причины, остальные каналы этим не трогаются. Смотреть — GET /api/v1/posts?group=…

Можно ли получить ссылку на опубликованное?

Поле externalUrl в ответе на статус. У ВКонтакте и X она есть всегда. У телеграма — только если канал публичный: ссылка собирается из его ника и номера сообщения, а у приватного канала ника нет. У MAX ссылки может не быть вовсе. Пустой externalUrl не значит, что пост не вышел, — смотрите state и publishedAt.

Можно постить в чужие каналы?

Нет. Ключ видит ровно те каналы, которые подключены к вашему аккаунту, и постит только в них. Подключить канал через API нельзя — живые шаги там чужие: диплинк в телеграме, вход во ВКонтакте через браузер, добавление бота руками в MAX. Это делается в кабинете, человеком.

Что делать, если ключ утёк?

Удалить его в кабинете на странице ключей — одной кнопкой, и он перестаёт работать сразу; взамен выпускается новый. Всё, что успели опубликовать до этого, останется опубликованным, поэтому ключу не место в репозитории и в переписке. В базе у нас лежит только SHA-256, показать вам старый ключ мы не можем даже при желании.

Есть SDK или библиотека под ваш API?

Нет, и не планируем. Это обычный HTTP с JSON: четыре ручки, один заголовок авторизации. Библиотека здесь была бы прослойкой над curl — вы напишете интеграцию быстрее, чем прочитаете её README. Машиночитаемое описание для тех, кому оно нужно, лежит на /openapi.json.

Как поставить пост на конкретное время?

Полем publishAt в формате ISO 8601 и обязательно со смещением зоны: 2026-09-06T09:00:00+03:00 — это девять утра по Москве. Без смещения время читается как всемирное, и пост уйдёт на три часа раньше, чем вы думали. Время в прошлом ошибкой не считается — такой пост отправщик возьмёт первым же тиком.

Можно ли править и снимать посты через API?

Через HTTP-ручки — пока нет: снаружи выставлены постановка в очередь и статус. Править и снимать умеет MCP-канал того же ключа (smmly_edit_post, smmly_cancel_post) и, конечно, кабинет. Если вам это нужно из кода — напишите, куда и зачем: очередь на выставление уже есть.

Постите из своего кода

7 дней полного доступа за 10 ₽ — подключите каналы, возьмите ключ и отправьте первый пост тем же curl, что выше.