Webhook API
для кросспостинга
Один HTTP-запрос — пост публикуется в Telegram, VK и Max одновременно. Bearer-токен, JSON, Idempotency-Key. Без OAuth, простой формат payload.
Webhook input — Pro+. Sandbox — все тарифы. Webhook output — Standard+.
Один эндпоинт
Вместо трёх API (Telegram Bot, VK, Max) — один webhook URL. Crosslybot конвертирует payload под каждую платформу автоматически.
LLM-friendly
Bearer-токен. ChatGPT, Claude, кастомные AI-агенты подключаются за минуты — стандартный паттерн REST.
Безопасно
Bearer-токен, Idempotency-Key, HMAC-подпись на исходящих, история запросов. Промышленный уровень безопасности.
Минимальный пример за 5 минут
Регистрация → создание webhook → cURL-команда. Полный гайд: /webhook/quickstart.
curl -X POST 'https://wh.crosslybot.ru/v1/webhooks/8qwv9XeTcR6Nyn2nlIW_wQ' \
-H 'Authorization: Bearer crossly_live_xxxxxxxxxxxxxxxxxxxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: hello-world-001' \
-d '{
"text": "Привет, мир! 👋",
"media": [
{"type": "photo", "url": "https://picsum.photos/800"}
],
"buttons": [[
{"text": "Подробнее", "url": "https://crosslybot.ru"}
]]
}' 200 OK · {"ok": true, "id": "post_..."} POST /v1/sandbox/test Сценарии использования
6 подробных гайдов под основные кейсы. От quick-start cURL до AI-агентов и no-code workflow.
Как опубликовать пост в Telegram через cURL за 5 минут
5 минут от регистрации до первого поста через API. cURL-команда внутри.
REST API для отправки постов в Telegram, VK и Max из своего бэкенда
Один webhook, три платформы. Bearer-токен, JSON, REST.
Автопостинг через AI-агента (ChatGPT, Claude) в Telegram, VK и Max
AI пишет — Crosslybot публикует. Один webhook, все соцсети.
Интеграция n8n с Telegram, VK и Max через webhook
n8n собирает данные — Crosslybot публикует. Один узел HTTP Request.
Публикация постов из Notion в Telegram, VK и Max автоматически
Контент-план в Notion → публикация в TG/VK/Max автоматически.
Кросспостинг RSS-фидов в Telegram, VK и Max
RSS приходит — Crosslybot публикует. Новостной агрегатор без копий-вставок.
Документация API
Полная спецификация: схема payload, заголовки, ответы, повторы, лимиты. Достаточно для production-интеграции.
Форматирование — в формате Telegram Bot API entities
Входящий webhook (IN) принимает форматирование текста в формате Telegram entities: offset, length, type — те же значения и та же семантика. Поддерживаются все 15+ типов: bold, italic, underline, strikethrough, spoiler, code, pre, blockquote, expandable_blockquote, text_link, mention, url, email, phone_number, hashtag, custom_emoji.
Если ваш код уже работает с Telegram Bot API — сериализацию entities переиспользуете как есть. Структура самих сообщений (медиа, альбомы, кнопки) у нас своя, единая для всех платформ — проще чем у TG. Полная таблица отличий ниже ↓
Исходящий webhook (OUT) отдаёт payload в том же формате что принимает IN — text (plain) + entities[] (TG-style массив, offsets/length в UTF-16). Симметрия: один и тот же формат — для отправки и для приёма. Если нужен HTML — конвертируете entities в теги на своей стороне (стандартная функция в TG-SDK).
Базовая информация
- Base URL:
https://wh.crosslybot.ru - Метод: POST
- Path:
/v1/webhooks/{slug} - Аутентификация: Bearer-токен в заголовке
Authorization - Content-Type:
application/json - Кодировка: UTF-8
- Размер запроса: до 5 МБ
HTTP-заголовки запроса
| Заголовок | Обяз. | Описание |
|---|---|---|
| Authorization | да | Bearer crossly_live_… или crossly_test_… Plaintext-значение показывается один раз при создании — сохраните его сразу. |
| Content-Type | да | application/json |
| Idempotency-Key | нет | UUID или произвольная уникальная строка. Повторный POST с тем же ключом за 24ч вернёт закэшированный ответ. Длина до 128 символов. |
| User-Agent | нет | Желательно указывать имя клиента (например, my-ai-agent/1.0) — отображается в истории запросов |
Поля JSON payload
Минимальный валидный пост: должен содержать хотя бы одно из text, media[] или buttons[].
| Поле | Тип | Описание |
|---|---|---|
| text | string | Текст поста. IN: plain text без HTML-тегов. OUT: plain (HTML-теги вырезаны). Лимит — самый щедрый из платформ (15895 для VK). |
| entities | array | Форматирование в формате Telegram Bot API: bold, italic, underline, strikethrough, spoiler, code, pre, blockquote, text_link, mention. Offsets/length в UTF-16 code units. Идентичный формат в IN (что принимаем) и в OUT (что шлём) — клиент учит формат один раз. |
| media[] | array | Медиа: до 10 элементов. Каждый — { type: photo|video|audio, url: https://… }. document — отклоняется (400). Альбом = массив media в одном POST'е (в отличие от TG Bot API, где альбом приходит как N updates с одинаковым media_group_id). Никакой буферизации не нужно — публикуется одним media group. |
| buttons[][] | array | URL-кнопки: ряды × кнопки (до 8×8). Каждая — { text, url }. URL должен начинаться с https:// или tg://. Telegram и Max публикуют как inline-кнопки. VK wall не поддерживает inline-кнопки — для VK включите URL прямо в текст или используйте подпись с ссылкой. |
| is_advertisement | bool | Маркер рекламного поста. Не обязателен для применения паузы — используется как метка для статистики |
| ad_pause_minutes | int | 0–1440. Пауза всего проекта после первой успешной публикации поста (применяется единожды). Уже принятые посты допубликуются, новые в проект не попадают до конца паузы |
| ad_target_pause_minutes | int | 0–1440. Глобальная пауза каналов опубликованных целей — действует во всех ваших проектах с этими каналами. Применяется в момент успеха публикации. Можно использовать вместе с ad_pause_minutes |
| external_id | string | Альтернатива Idempotency-Key (можно использовать вместе). До 128 символов |
| trace_id | string | Произвольный ID для end-to-end отладки. Возвращается в response и логах |
| metadata | object | Произвольные поля. Сохраняются в Post.extra_data |
Лимиты и валидация медиа
- 📷 photo: до 20 МБ (jpg/png/webp)
- 🎬 video: до 2 ГБ, до 30 мин (mp4/mov/webm)
- 🎵 audio: до 500 МБ (mp3/m4a/ogg)
- 📑 document: отклоняется (400)
- 📦 Сумма media[]: до 4 ГБ
- 🔢 Количество: до 10 элементов
- 🌐 URL: только https://, ≤ 2048 символов, прямая ссылка на файл без редиректов
- 🛡 Decompression bomb: ≤ 25 МП у фото
400 с деталями в errors[]. Документы и приватные адреса отклоняются.
⚠ Редиректы на файл запрещены — отдавайте прямую ссылку на файл. Если ваш CDN/хостинг возвращает редирект — публикация падает с ошибкой.
HTTP-коды ответов
| Код | Значение | Действие клиента |
|---|---|---|
| 200 | Принят, поставлен в очередь | Готово |
| 202 | Уже принят (Idempotency) | Тот же результат — без действий |
| 400 | Невалидный payload | Прочитать errors[] и исправить |
| 401 | Slug не найден или Bearer невалиден (одинаковый ответ для защиты от энумерации) | Проверить URL и токен. Перевыпустить токен в /channels |
| 402 | Тариф не позволяет | Улучшить тариф / купить пакет постов |
| 403 | Endpoint деактивирован | Активировать в UI |
| 422 | Невалидная JSON-схема | Прочитать errors[] в теле — там детали по каждому полю |
| 429 | Превышен rate limit | Учитывать Retry-After header |
| 503 | Webhook API временно недоступен | Retry с backoff |
| 5xx | Внутренняя ошибка | Retry с экспоненциальным backoff |
Decisions в «Истории запросов»
В UI карточки endpoint'а есть журнал последних 100 запросов. Каждая запись содержит decision — внутренняя причина решения. Это поможет отладить интеграцию когда клиент видит просто 401 или 429:
| Decision | HTTP | Что случилось |
|---|---|---|
| accepted | 200 | Принят, пост создан и поставлен в очередь публикации |
| accepted_no_project | 200 | Принят, но webhook не привязан ни к одному проекту — пост не создаётся (silent) |
| accepted_idempotent_replay | 200 | Дубль по Idempotency-Key, отдан кешированный ответ |
| rejected_auth | 401 | Slug не существует, токен неверен, IP вне allowlist, signature не сошлась или timestamp вне окна — клиенту отдаётся одинаковый ответ, точная причина в журнале |
| rejected_validation | 400/415/422 | Невалидный JSON, неподходящий Content-Type, payload >5MB или схема не прошла валидацию (детали в errors[]) |
| rejected_rate_limit | 429 | Превышен один из лимитов: min-interval, burst или часовая квота. Используйте Retry-After |
| rejected_paused | 503 | Endpoint выключен (вручную или автопауза после rate-limit нарушений) |
| rejected_tier | 503 | Текущий тариф больше не разрешает webhook IN — endpoint автоматически отключён |
| rejected_disabled | 503 | Webhook API временно недоступен (плановые работы) |
Примеры кода
Замените 8qwv9XeTcR6Nyn2nlIW_wQ на свой slug (копируется из карточки в /channels) и crossly_live_... на ваш токен.
curl -X POST https://wh.crosslybot.ru/v1/webhooks/8qwv9XeTcR6Nyn2nlIW_wQ \
-H "Authorization: Bearer crossly_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"text": "Привет, мир!",
"entities": [{"type": "bold", "offset": 0, "length": 6}],
"media": [
{"type": "photo", "url": "https://cdn.example.com/img.jpg"}
],
"external_id": "post-123",
"metadata": {"source": "ai-agent"}
}' import os, time, uuid, requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(
total=4,
backoff_factor=2, # 2с / 4с / 8с / 16с
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST"],
respect_retry_after_header=True,
)))
SLUG = "8qwv9XeTcR6Nyn2nlIW_wQ"
TOKEN = os.environ["CROSSLY_TOKEN"]
resp = session.post(
f"https://wh.crosslybot.ru/v1/webhooks/{SLUG}",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
"User-Agent": "my-ai-agent/1.0",
},
json={
"text": "Привет, мир!",
"entities": [{"type": "bold", "offset": 0, "length": 6}],
"media": [{"type": "photo", "url": "https://cdn.example.com/img.jpg"}],
"external_id": "post-123",
},
timeout=30,
)
resp.raise_for_status()
print(resp.json()) import { randomUUID } from "crypto";
const SLUG = "8qwv9XeTcR6Nyn2nlIW_wQ";
const TOKEN = process.env.CROSSLY_TOKEN;
const BACKOFFS = [2000, 4000, 8000, 16000]; // 2c / 4с / 8с / 16с
async function publish(payload) {
for (let attempt = 0; attempt < BACKOFFS.length + 1; attempt++) {
const resp = await fetch(
`https://wh.crosslybot.ru/v1/webhooks/${SLUG}`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
"User-Agent": "my-ai-agent/1.0",
},
body: JSON.stringify(payload),
}
);
if (resp.ok) return resp.json();
if (![429, 500, 502, 503, 504].includes(resp.status)) {
throw new Error(`Crosslybot ${resp.status}: ${await resp.text()}`);
}
if (attempt === BACKOFFS.length) throw new Error("Max retries exceeded");
const ra = Number(resp.headers.get("retry-after")) * 1000;
await new Promise(r => setTimeout(r, ra || BACKOFFS[attempt]));
}
}
await publish({
text: "Привет, мир!",
media: [{ type: "photo", url: "https://cdn.example.com/img.jpg" }],
external_id: "post-123",
}); Sandbox: тестирование без публикации
Sandbox — отдельный эндпоинт для проверки формата payload и валидации медиа без записи в БД и без публикации в каналы. Ответ совпадает по структуре с боевым: видно какие ошибки, какие цели получили бы пост, какая обрезка текста применилась бы под каждую платформу.
- URL:
POST /v1/sandbox/test - Аутентификация: тот же Bearer токен (live или test)
- Payload: идентичный боевому
- Не списывает квоту постов, не отображается в истории публикаций
- Что проверяется: JSON-схема, entities, лимиты длины, https://, тип и реальный mime-type медиа
- Что НЕ проверяется: реальная доставка в Telegram/VK/Max API (только локальная валидация)
- Лимиты Free: 100 запросов/день, Mini: 1 000/день, Standard+: ∞
curl -X POST https://wh.crosslybot.ru/v1/sandbox/test \\
-H "Authorization: Bearer crossly_test_xxxxxxxxxxxxxxxxxxxx" \\
-H "Content-Type: application/json" \\
-d '{
"text": "Тестовый пост",
"entities": [{"type": "bold", "offset": 0, "length": 8}],
"media": [
{"type": "photo", "url": "https://cdn.example.com/img.jpg"}
]
}' {
"ok": true,
"would_create_post": true,
"validation": {
"schema": "passed",
"media": [{"url": "https://cdn.example.com/img.jpg", "size_mb": 2.4, "ok": true}],
"entities": "passed"
},
"preview": {
"telegram": {"text_after_trim": "Тестовый пост", "trimmed_chars": 0},
"vk": {"text_after_trim": "Тестовый пост", "trimmed_chars": 0},
"max": {"text_after_trim": "Тестовый пост", "trimmed_chars": 0}
}
} 💡 Используйте sandbox в CI/CD как smoke-тест после каждого деплоя интеграции — это бесплатный способ убедиться что payload-генератор не сломан.
Совместимость с Telegram Bot API
Формат payload максимально близок к Telegram Bot API — если у вас уже есть код для TG, большую часть можно переиспользовать. Но есть отличия в моделях, перечислены ниже.
| Что | У TG Bot API | У нас |
|---|---|---|
| Форматирование (entities) | type/offset/length/url/language/custom_emoji_id, offset в UTF-16 | ✓ Тот же формат |
| Альбом | N отдельных update'ов с общим media_group_id | Один POST с массивом media[] (атомарно, без буферизации) |
| Текст и подпись медиа | Раздельно: text для сообщения, caption для медиа | Один text — он же caption когда есть media[] |
| Медиа | file_id или различные структуры (photo[], video, document) | {type, url} — единая структура для photo/video/audio |
| Кнопки | inline_keyboard с callback, switch_inline и пр. | Только URL-кнопки {text, url} (callback не портируется на VK/Max). Публикуются в TG и Max; в VK wall не поддерживается — игнорируется. |
| Идемпотентность | update_id в payload | Заголовок Idempotency-Key или поле external_id (cache 24h) |
| Опросы (poll) | Поддерживаются | Не поддерживаются (опросы не работают на VK/Max единообразно) |
| Документы | Любые файлы | Отклоняются (400) — security и совместимость с другими платформами |
💡 Если переносите код с TG Bot API: основная адаптация — собирайте альбом в один POST с media[] вместо последовательных send'ов. Entities и URL-кнопки переиспользуются как есть.
Адресная публикация и отложенные паузы
Три независимых механизма управления публикацией прямо из payload — комбинируйте их свободно.
1. Адресная публикация — targets[]
По умолчанию пост идёт во все активные цели проекта. Чтобы ограничить — передайте targets[] с public_id целей (копируется из карточки цели).
{
"text": "Анонс — только в основные каналы",
"targets": [
{ "id": "tgt_aBcDeFgHiJkLmNoPqRsTuV" },
{ "id": "tgt_xYz123456789abcdefGhi" }
]
}
⚠️ Если ни один из переданных id не совпал с целями проекта — ответ 422 invalid_targets, пост не создаётся.
2. Автопауза проекта — ad_pause_minutes
После первой успешной публикации поста весь проект ставится на паузу на N минут. Применяется один раз — повторные post-target'ы того же поста (multi-target) паузу не продлевают.
{
"text": "Рекламная вставка",
"is_advertisement": true,
"ad_pause_minutes": 60
} 3. Автопауза канала — ad_target_pause_minutes
Каждый успешно опубликованный канал ставится на глобальную паузу — действует во всех ваших проектах с этим каналом. Удобно когда контракт с площадкой требует «не больше одной рекламы в час»: параллельный обычный кросспостинг в этот канал тоже подождёт.
{
"text": "Реклама в TG-канал на 24 часа",
"is_advertisement": true,
"ad_target_pause_minutes": 1440,
"targets": [
{ "id": "tgt_aBcDeFgHiJkLmNoPqRsTuV" }
]
} - • Поля
ad_pause_minutesиad_target_pause_minutesнезависимы — можно одно, оба или ни одного. - • Триггер автопаузы — фактическая успешная публикация. Если все цели упали — пауза не применяется.
- • Пауза цели — глобальная на канал: действует во всех ваших проектах, где этот канал используется. Параллельный обычный кросспостинг в тот же канал тоже подождёт.
- • Существующая ручная пауза не сокращается. Авто-пауза только продлевает, если новая позже.
- •
is_advertisement— метка для статистики, не обязательна для работы пауз.
Пауза без публикации — temp-pause / temp-resume
Автопаузы выше срабатывают вместе с публикацией поста. Если нужно поставить паузу отдельно (например рекламная площадка хочет «забронировать тишину» наперёд) — есть два endpoint'а, которые ничего не публикуют, только управляют паузой.
POST /v1/webhooks/{slug}/temp-pause
{
"project_minutes": 60, // пауза всего проекта (опционально)
"target_minutes": 120, // глобальная пауза каналов (опционально)
"targets": ["tgt_aBc..."] // какие каналы; пусто = все цели проекта
} - • Минимум одно из
project_minutes/target_minutesдолжно быть > 0. - •
target_minutes— глобальная пауза ресурса: действует во всех ваших проектах с этим каналом. - • Если
targetsуказан, но ни одинidне принадлежит проектам этого webhook'а — 422 invalid_targets. - • Идемпотентно: не сокращает уже активную более длинную паузу.
POST /v1/webhooks/{slug}/temp-resume
{
"scope": "all", // "project" | "targets" | "all" (default all)
"targets": ["tgt_aBc..."] // какие каналы снять (если scope=targets)
} Накопленные за время паузы посты публикуются с интервалом 1 мин после снятия. Оба endpoint'а используют тот же auth-стек (Bearer + опциональный HMAC + timestamp), что и публикация.
Ответ: {ok, request_id, projects_affected, resources_affected, rescheduled_post_targets}.
Discovery endpoint — для MCP и автоматизаций
Чтобы MCP-серверам (Claude Desktop, Cursor) и скриптам не приходилось вручную копировать tgt_… каждой цели и знание о платформах — есть один запрос, который отдаёт всю структуру:
curl https://wh.crosslybot.ru/v1/webhooks/{slug}/info \
-H "Authorization: Bearer crossly_live_..." Ответ (режим по умолчанию — обезличенный):
{
"endpoint": { "slug": "...", "name": null, "verbose": false },
"projects": [
{
"index": 1,
"name": null,
"paused_until": null,
"targets": [
{ "id": "tgt_aBc...", "platform": "telegram", "name": null, "paused_until": null },
{ "id": "tgt_xYz...", "platform": "max", "name": null, "paused_until": null }
]
}
],
"capabilities": {
"entity_types": ["bold","italic","spoiler", ...],
"media_types": ["photo","video","audio"],
"limits": {
"max_text_length": 15895, "max_media": 10,
"max_buttons_rows": 8, "max_buttons_per_row": 8,
"max_payload_bytes": 5242880, "max_pause_minutes": 1440
}
}
} Verbose режим — включается в карточке webhook IN (раздел «Безопасность» → toggle «Раскрывать имена в discovery (/info)»). Тогда в ответе вместо null появятся реальные названия проекта, целей и самого endpoint'а.
- • Безопасность идентична POST: те же Bearer, IP allowlist, timestamp, HMAC (для GET подписывается
method\npath\ntimestampвместо body), tariff guard. - • Отдельный rate-limit 60 запросов/мин — не съедает publishing budget. Violations пишутся в общий счётчик soft-block, атакующий не может через flood /info обойти автозащиту.
- •
paused_untilдля проекта и цели всегда видим — клиент знает что пост встанет в очередь, не публикуется сразу. - •
capabilities.limits— клиент-валидатор может проверить текст/медиа/паузы до отправки и избежать 422.
⚠️ Не возвращаем точные значения rate-limit (min_interval, hourly_quota) — раскрытие даёт атакующему с украденным токеном protocol cheatsheet.
MCP-сервер — для Claude Desktop, Cursor, Cline
Подключите Crosslybot напрямую к AI-ассистенту — он сможет публиковать посты, искать цели по именам, ставить каналы на паузу через простые команды.
Hosted-вариант (рекомендуется) — никаких локальных установок, только URL в конфиге AI-клиента:
// claude_desktop_config.json
{
"mcpServers": {
"crosslybot": {
"url": "https://mcp.crosslybot.ru/sse/{slug}",
"headers": {
"Authorization": "Bearer crossly_live_..."
}
}
}
} Слаг и токен — из карточки webhook IN в личном кабинете. Включите toggle «Раскрывать имена в discovery (/info)» чтобы AI мог находить цели по именам.
Tools AI получает автоматически:
- •
crosslybot_discover— список ваших проектов и целей публикации (Telegram/VK/Max). - •
crosslybot_publish— публикация поста. Принимаетtgt_…или имя цели ("маркетинг"). Поддерживает все поля payload включая автопаузу проекта/цели после публикации.
С HMAC-защитой — добавьте ещё один header:
"headers": {
"Authorization": "Bearer crossly_live_...",
"X-Crosslybot-Hmac-Secret": "..."
}
⚠️ Если включён IP allowlist, добавьте в whitelist IP нашего MCP-сервера: 89.223.125.61.
Self-hosted доступен через Docker (multi-arch) — см. GitHub или образ ghcr.io/antiblef/crosslybot-mcp. Документация: /mcp.
Идемпотентность
Заголовок Idempotency-Key или поле external_id в payload — стандартный способ безопасных повторов запросов.
- • Ключи кешируются на 24 часа в рамках endpoint
- • Повторный POST с тем же ключом возвращает 202 и закэшированный ответ — БЕЗ создания дубликата поста
- • Если первый запрос упал на 5xx и не был обработан — повтор создаст пост впервые
- • Длина ключа до 128 символов. Рекомендуется UUIDv4 (универсально) или бизнес-ID (стабильно для логики)
Политика повторов (для клиентов)
Когда повторять запрос, и через какое время.
| Ситуация | Повторять? | Стратегия |
|---|---|---|
| 200 / 202 | Нет | Готово |
| 4xx (кроме 429) | Нет | Это ошибка вашего клиента — повторы не помогут. Прочитайте errors[] |
| 429 | Да | Учитывайте Retry-After header. Если его нет — backoff 2с/4с/8с/16с |
| 503 | Да | Backoff 5с/15с/45с/2 мин/5 мин — не более 5 попыток |
| 5xx (кроме 503) | Да | Экспоненциальный backoff 2с/4с/8с/16с — макс 4 попытки. Всегда тот же Idempotency-Key |
⚠ ВАЖНО: при retry используйте тот же Idempotency-Key — иначе создастся дубликат поста.
Rate limit и квоты
Четырёхуровневая защита per-slot: настраиваемый интервал + burst + часовая квота по тарифу + автопауза при систематических нарушениях. Превышение → 429 Too Many Requests с заголовком Retry-After.
| Тариф | Min-interval | Burst (per slot) | Часовая квота | Webhook IN |
|---|---|---|---|---|
| Free / Mini / Standard | — | — | — | недоступен |
| Pro | 1–60 сек | 10 req/sec | 100 | ✓ |
| Maxi | 1–60 сек | 10 req/sec | 300 | ✓ |
| Business | 1–60 сек | 10 req/sec | 1 000 | ✓ |
- • Per-slot изоляция: лимиты привязаны к URL входящего webhook'а (slug), а не к аккаунту — атака на один webhook не задевает другие.
- • Min-interval: «не чаще одного запроса в N секунд», настраивается в карточке endpoint'а (по умолчанию 10s). Защита от случайного флуда из-за бага в bot'е/скрипте отправителя.
- • Burst: sliding window 1 секунда. Защита от мгновенного флуда.
- • Часовая квота: сбрасывается на границе UTC-часа.
- • Автопауза: после 5 нарушений лимита за час (любого уровня — min-interval, burst или часовая квота) endpoint автоматически выключается (
is_active=false). Владельцу уведомление в Telegram-бот. Включается обратно вручную через UI после устранения причины — авто-восстановления нет, чтобы цикл «пауза→повторный абьюз→пауза» не повторялся.
Лимит постов (как у обычных целей): каждая успешная публикация webhook-поста = 1 пост в месячной квоте тарифа. Не путайте с rate limit (защита от абьюза) и квотой постов (биллинг).
Дополнительные защиты webhook IN
Помимо HTTPS + Bearer-токена, в карточке endpoint'а доступны опциональные защиты для повышенной безопасности.
- HMAC-подпись от отправителя. Defense-in-depth: отправитель подписывает body своим секретом, мы проверяем. Даже если Bearer-токен утечёт — без секрета подделать запрос не получится. Включается toggle в карточке endpoint'а. Проверка идентична нашему OUT-варианту (см. ниже про Webhook OUT) — алгоритм один, клиенту достаточно одной функции для приёма и отправки. Примеры кода — ниже.
- Replay protection через timestamp.
При
require_timestamp=onтребуем заголовокX-Crosslybot-Timestamp(Unix seconds) в окне ±5 мин. Защита от replay-атак если body+token утекли в логи или были перехвачены: запрос валиден только в 5-минутном окне, потом —401. - IP allowlist (CIDR).
Список IP/CIDR из которых принимаем запросы. Запросы с других адресов получают идентичный
401(timing-safe, не раскрываем что allowlist настроен). Имеет смысл если у отправителя статичный IP — для облачных SaaS (n8n cloud, Zapier, Make, AWS Lambda) IP-пулы плавающие, тогда лучше не использовать. - Ротация публичного URL.
Если адрес webhook'а попал в открытые источники — нажмите «Rotate URL» в карточке. Сгенерируется новый
/v1/webhooks/<new-slug>, старый мгновенно перестаёт работать. Cooldown 24 ч от случайных кликов. Endpoint, токен и все привязки в проектах при ротации сохраняются — после ротации просто обновите URL у всех отправителей. - История запросов в UI. На странице endpoint'а — последние 100 запросов с фильтрами по статусу. Виден IP, decision, причина, latency, размер payload. Удобно для отладки интеграции и расследования инцидентов.
Подпись запроса от отправителя (если включён require_signature)
Алгоритм: HMAC-SHA256(secret, raw_body) → hex → заголовок X-Crosslybot-Client-Signature: sha256=<hex>.
Принимается также формат без префикса sha256= и регистр hex (UPPERCASE/lowercase).
Подпись валидируется до основной обработки запроса; при несовпадении — идентичный 401 (как для невалидного токена, timing-safe).
SECRET="ваш-сгенерированный-secret"
BODY='{"text":"привет"}'
SIG=$(printf "%s" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -X POST https://wh.crosslybot.ru/v1/webhooks/<slug> \
-H "Authorization: Bearer crossly_live_..." \
-H "Content-Type: application/json" \
-H "X-Crosslybot-Client-Signature: sha256=$SIG" \
--data-binary "$BODY" import hmac, hashlib, json, requests
SECRET = "ваш-secret"
body_bytes = json.dumps({"text": "привет"}).encode("utf-8")
signature = hmac.new(SECRET.encode(), body_bytes, hashlib.sha256).hexdigest()
resp = requests.post(
"https://wh.crosslybot.ru/v1/webhooks/<slug>",
headers={
"Authorization": "Bearer crossly_live_...",
"Content-Type": "application/json",
"X-Crosslybot-Client-Signature": f"sha256={signature}",
},
data=body_bytes, # ВАЖНО: data=, не json= — нужны точные байты
) import { createHmac } from "crypto";
const SECRET = "ваш-secret";
const body = JSON.stringify({ text: "привет" });
const signature = createHmac("sha256", SECRET).update(body).digest("hex");
await fetch("https://wh.crosslybot.ru/v1/webhooks/<slug>", {
method: "POST",
headers: {
"Authorization": "Bearer crossly_live_...",
"Content-Type": "application/json",
"X-Crosslybot-Client-Signature": `sha256=${signature}`,
},
body, // ВАЖНО: точные байты, не пересериализовывать
}); import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
)
secret := []byte("ваш-secret")
body := []byte(`{"text":"привет"}`)
mac := hmac.New(sha256.New, secret)
mac.Write(body)
sig := hex.EncodeToString(mac.Sum(nil))
req, _ := http.NewRequest("POST", "https://wh.crosslybot.ru/v1/webhooks/<slug>", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer crossly_live_...")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Crosslybot-Client-Signature", "sha256="+sig)
http.DefaultClient.Do(req)
⚠ Подпись считается от точных байтов body. Если ваша HTTP-библиотека пересериализует JSON между подписью и отправкой — подпись не сойдётся, получите 401. Используйте data=bytes / --data-binary / эквиваленты.
Webhook OUT: проверка HMAC-подписи
Когда Crosslybot отправляет пост на ваш URL (исходящий webhook), мы подписываем тело запроса HMAC-SHA256 и кладём подпись в заголовок X-Crosslybot-Signature. Это позволяет приёмнику убедиться что запрос действительно от Crosslybot.
X-Crosslybot-Signature: sha256=<hex>
X-Crosslybot-Delivery-Id: <uuid>
X-Crosslybot-Timestamp: <unix-seconds>
X-Crosslybot-Event: post.published
Idempotency-Key: <uuid>
Content-Type: application/json import hmac, hashlib
def verify(secret: str, raw_body: bytes, header_signature: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"), raw_body, hashlib.sha256
).hexdigest()
# constant-time сравнение чтобы избежать timing-атак
return hmac.compare_digest(expected, header_signature)
# Пример (Flask):
# raw = request.get_data()
# sig = request.headers["X-Crosslybot-Signature"]
# if not verify(SECRET, raw, sig): abort(401) Управление токенами
- • Plaintext показывается ОДИН раз — при создании или rotate. Сохраните сразу в свой secret-менеджер
- • Префиксы:
crossly_live_для прода,crossly_test_для тестирования (легко искать в логах) - • Rotate: мгновенная замена. Старый токен немедленно перестаёт работать
- • Revoke: в UI можно отозвать токен или временно деактивировать endpoint
- • Несколько токенов: в MVP — один активный токен на endpoint. Если нужно дать токен второй интеграции — создайте отдельный webhook
Best practices
- ✓ Хранение токена: только в env-переменных или secret-менеджерах. Не в коде, не в Git
- ✓ Idempotency-Key всегда: даже если сейчас не делаете retry. Защитит от случайных дубликатов при сбоях
- ✓ User-Agent: укажите имя клиента — упростит отладку через историю запросов
- ✓ trace_id: сквозной идентификатор от вашего сервиса до публикации — полезно для распределённого трейсинга
- ✓ Sandbox перед прод: POST /v1/sandbox/test для CI/CD smoke-тестов на каждом деплое
- ✓ Лимит размера media: сжимайте до отправки если знаете что цели имеют меньшие лимиты, чтобы избежать дополнительной AI-обработки
- ✓ Мониторинг ответов: логируйте status_code и trace_id из 200/202 ответов
- ✓ Не блокируйте свой код: отправка webhook должна быть фоновой задачей с retry, а не в hot-path обработки запроса пользователя
Тарифы и лимиты
Sandbox-тестер доступен на любом тарифе. Реальный приём webhook'ов и доставка наружу — на платных.
| Возможность | Free | Mini | Standard | Pro | Maxi | Business |
|---|---|---|---|---|---|---|
| Sandbox /v1/sandbox/test | 100/день | 1k/день | ∞ | ∞ | ∞ | ∞ |
| Webhook output | — | — | ✓ | ✓ | ✓ | ✓ |
| Webhook input | — | — | — | ✓ | ✓ | ✓ |
| Burst rate (input) | — | — | — | 1/5с | 1/2с | 1/1с |
| Quota / hour (input) | — | — | — | 300 | 1 000 | 5 000 |
Готов попробовать?
Создайте webhook за минуту, протестируйте формат через sandbox, подключите AI или свой бэкенд.