Go и Python SDK
Официальные SDK закрывают ту часть API, которую программы дёргают чаще
всего: отправку сообщений, вопросы с ожиданием ответа,
heartbeat, мониторы и приём
метрик. Методы типизированы, ошибки разобраны, повторы с
Retry-After уже внутри — собирать HTTP-запросы к REST API
руками не нужно.
- Go — модуль
github.com/truboter/notifly.ru/sdk/go, единственная зависимость —github.com/coder/websocket(сама без зависимостей): по сокету SDK получает сигнал о готовом ответе на вопрос. - Python — пакет, который импортируется как
notifly, Python 3.9+, единственная зависимость —requests.
Установка
Заголовок раздела «Установка»С исходниками на руках Go-модуль подключается директивой replace в go.mod,
а Python-пакет ставится из каталога с его pyproject.toml (pip install <каталог>).
Импорты в коде такие:
import notifly "github.com/truboter/notifly.ru/sdk/go"from notifly import NotiflyКакой токен куда
Заголовок раздела «Какой токен куда»Клиент создаётся с токеном, и именно вид токена решает, что клиенту можно.
Не тот вид — 401; верный вид без нужного права (например, отправка шарой
«только просмотр») — 403. Подробно про выпуск и скоуп — на странице
API-токены.
| Токен | Префикс | Куда передаётся | Что открывает |
|---|---|---|---|
| API-токен | M | WithAPIToken(…) / Notifly("M…") | всё на уровне аккаунта: каналы, мониторы, heartbeat-ы, сканы, история, сообщения |
| Токен канала или шары | A / S | WithChannelToken(…) / Notifly("A…") | один канал: отправка и ask-цикл (нужно право записи, иначе 403), чтение сообщений и подписка по сокету (нужно право чтения) |
| Токен устройства | C | туда же, куда API-токен | те же маршруты, но это сессия устройства, а не ключ для интеграций |
| Ping-токен heartbeat | H | аргументом вызова | HeartbeatPing / heartbeat_ping |
| Токен источника метрик | M | аргументом вызова | PushMetric / push_metric — несмотря на префикс, это не API-токен |
| Токен роутера | R | аргументом вызова | TriggerRouter / trigger_router — вызов ingest-URL роутера вебхуков |
Отправка сообщения
Заголовок раздела «Отправка сообщения»client := notifly.New(notifly.WithChannelToken("A<channel-token>")) // defaults to https://api.notifly.ru
msg, err := client.Send(ctx, notifly.Message{ Title: "Deploy finished", Body: "v1.4.2 is live", Priority: 7, // 0 = use the channel default Extras: map[string]any{ "client::display": map[string]any{"contentType": "text/markdown"}, },})nf = Notifly("A<channel-token>") # defaults to https://api.notifly.ru
nf.send("Deploy finished", "v1.4.2 is live", priority=7)
# Markdown and other extras:nf.send("Report", "**bold** text", extras_dict={ "client::display": {"contentType": "text/markdown"},})Приоритет, заголовок и дополнительные поля — как в Отправке сообщений и Доп. полях.
Вопрос с ожиданием ответа
Заголовок раздела «Вопрос с ожиданием ответа»Скрипт задаёт вопрос и ждёт, пока человек нажмёт кнопку в приложении — получается gate перед опасным шагом (подробности — Вопросы).
q, err := client.Ask(ctx, notifly.AskRequest{ Question: "Deploy v1.4.2 to production?", Options: []string{"deploy", "abort"}, DefaultOption: "abort", TimeoutSec: 600, // auto-expire after 10 minutes DefaultAnswer: "abort", // submitted automatically on timeout})
waitCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)defer cancel()ans, err := client.WaitAskAnswer(waitCtx, q.ID, 3*time.Second)if ans.Status == notifly.AskAnswered && ans.Answer == "deploy" { // approved}q = nf.ask( "Deploy v1.4.2 to production?", options=["deploy", "abort"], default_option="abort", timeout_sec=600, default_answer="abort",)
ans = nf.wait_ask_answer(q["id"], poll=3, timeout=900)if ans["status"] == "answered" and ans["answer"] == "deploy": ... # approvedВ Go WaitAskAnswer подписывается на вопрос по WebSocket и возвращается
примерно через секунду после того, как человек нажал кнопку. Опрашивать раз в
pollInterval он начинает только там, где сокет открыть не удалось: свой сервер
без /ws, прокси без upgrade, токен канала без права чтения. Отключить сокет
совсем — notifly.WithPollingOnly(); эта опция выключает и Subscribe (он
сразу вернёт ErrWebSocketUnavailable). Python-SDK опрашивает всегда, а
wait_ask_answer, не дождавшись ответа за timeout, бросает NotiflyError —
сам вопрос при этом остаётся открытым.
GetAskAnswer / get_ask_answer — один опрос, если цикл ожидания вы пишете
сами; статусы pending, answered, expired, cancelled. Воркеру, который
разбирает много вопросов сразу, подойдут PendingAnswers + AckAnswers
(pending_answers + ack_answers). Продлить дедлайн — ExtendAsk
(extend_ask_question), он ставит новый срок «сейчас + N секунд», а не
добавляет к остатку.
У AskRequest есть и другие поля: Fields — форма вместо кнопок (ответ
приходит JSON-объектом), Next — ветвление: какой вопрос задать следом при
выборе варианта (его id вернётся в NextQuestionID), AllowMulti,
JSONFormat, ClientMessageID — ключ идемпотентности, CallbackURL и
CallbackSecret — вебхук на итог вопроса. В Python это одноимённые аргументы
ask(): fields, next, allow_multi, json_format, client_message_id,
callback_url, callback_secret.
Ответить на вопрос самому — AnswerAsk / AnswerAskMulti
(answer_ask_question): так автоматизация снимает собственный gate, когда её
проверки прошли. Отозвать ещё открытый вопрос — CancelAsk
(cancel_ask_question). Оба действуют от имени аккаунта, поэтому берут не
токен канала, а API-токен.
Управление аккаунтом: каналы, мониторы, heartbeat
Заголовок раздела «Управление аккаунтом: каналы, мониторы, heartbeat»Это уже API-токен.
admin := notifly.New(notifly.WithAPIToken("M<api-token>"))
ch, err := admin.CreateChannel(ctx, notifly.ChannelParams{ Name: "deploy-bot", Description: "CI notifications",})fmt.Println("channel token:", ch.Token) // shown only once
_, err = admin.CreateMonitor(ctx, notifly.MonitorParams{ ChannelID: ch.ID, Name: "example.com", Kind: notifly.KindHTTP, // 27 kinds, see the Kind* constants Target: "https://example.com", IntervalSec: 60, AlertMessage: "example.com is down",})
hb, err := admin.CreateHeartbeat(ctx, notifly.HeartbeatParams{ ChannelID: ch.ID, Name: "nightly-backup", IntervalSec: 24 * 3600, GraceSec: 3600, // tolerate an hour of lateness AlertMessage: "nightly backup did not report",})// hb.PingToken is a capability for this heartbeat alone — all the job needs:err = admin.HeartbeatPing(ctx, hb.PingToken)admin = Notifly("M<api-token>")
ch = admin.create_channel("deploy-bot", description="CI notifications")print("channel token:", ch["token"]) # shown only once
admin.create_monitor( channel_id=ch["id"], name="example.com", kind="http", # 27 kinds — see MONITOR_KINDS target="https://example.com", interval_sec=60, alert_message="example.com is down",)
hb = admin.create_heartbeat( channel_id=ch["id"], name="nightly-backup", interval_sec=24 * 3600, alert_message="nightly backup did not report", grace_sec=3600,)admin.heartbeat_ping(hb["pingToken"])Шесть семейств мониторов устроены одинаково — List/Create/Update/Delete/Pause/Resume:
обычный (Monitor), HTTP-запрос (HTTPMonitor), TCP-порты (PortMonitor),
контент страницы (ContentMonitor), многошаговый HTTP (WorkflowMonitor) и
сценарий в браузере (BrowserWorkflow). У всех, кроме
браузерного, есть Test* — проверить цель один раз, ничего не создавая;
MonitorHistory и MonitorLog показывают, почему монитор «мигает».
Канал, созданный самим токеном, попадает в его скоуп — иначе CreateChannel
создавал бы канал, которого создатель тут же не видит.
Любой не-2xx ответ становится ошибкой с разобранным телом. Проверять статус числом не нужно — есть предикаты.
_, err := client.Send(ctx, notifly.Message{Body: "hi"})switch {case err == nil:case notifly.IsRateLimited(err): // 429 var apiErr *notifly.APIError errors.As(err, &apiErr) time.Sleep(apiErr.RetryAfter)case notifly.IsAuth(err): // 401/403 — wrong, revoked or read-only token log.Fatal("check the token kind and its scope")case notifly.IsQuotaExceeded(err): // 402 — the plan does not allow this log.Fatal(err)}import time
from notifly import NotiflyError
try: nf.send("t", "m")except NotiflyError as exc: if exc.is_rate_limited: # 429 time.sleep(exc.retry_after or 5) elif exc.is_auth: # 401/403 raise SystemExit("check the token kind and its scope") elif exc.is_quota_exceeded: # 402 raise SystemExit("the plan does not allow this") else: raiseЕщё есть IsNotFound / is_not_found (404) и StatusCode(err) в Go —
сырой код, 0 для транспортной ошибки. Тело больше 1 МиБ в Go не режется, а
возвращается как ErrResponseTooLarge: запросите страницу поменьше.
Повторы, таймауты и настройки клиента
Заголовок раздела «Повторы, таймауты и настройки клиента»По умолчанию — до двух повторов. 429 повторяется всегда (запрос отклонён, а
не выполнен), транспортные ошибки и ответы 502, 503, 504 — только для
идемпотентных GET, поэтому отправка сообщения никогда не задваивается молча.
500 не повторяется. Паузы берутся из Retry-After, иначе — экспоненциальный
backoff (0,5 с, 1 с, 2 с… не больше 5 с). Python не ждёт на одной паузе дольше
30 секунд (MAX_RETRY_WAIT), даже если Retry-After просит больше.
client := notifly.New( notifly.WithChannelToken("A<channel-token>"), notifly.WithAPIToken("M<api-token>"), notifly.WithBaseURL("https://my-notifly.example.com"), notifly.WithHTTPClient(&http.Client{Timeout: 5 * time.Second}), notifly.WithUserAgent("my-deploy-bot/1.0"), notifly.WithRetry(0), // disable retries notifly.FromEnv(), // fill what is still empty from the environment)nf = Notifly( "A<channel-token>", base_url="https://my-notifly.example.com", timeout=5.0, retries=0, user_agent="my-deploy-bot/1.0",)FromEnv() в Go читает NOTIFLY_API_TOKEN, NOTIFLY_CHANNEL_TOKEN и
NOTIFLY_DEFAULT_BASE_URL, но только для полей, которые ещё пусты, — явная
опция всегда сильнее. Чтение окружения намеренно не встроено в New: клиент не
должен подхватывать чужой токен из шелла сам по себе. В Python переменных
окружения нет — токен передаётся аргументом.
Ещё несколько настроек Go-клиента. WithLogger(*slog.Logger) включает
диагностику, которая по умолчанию выброшена: на уровне Debug — строка на
каждый запрос и жизненный цикл сокета, на Warn — повторы; токены маскируются,
тела запросов и ответов не пишутся. WithWSURL задаёт адрес сокета, когда он
живёт не на /ws хоста API (например, URL стейджа AWS API Gateway); без неё
адрес выводится из базового: https → wss, путь /ws. Python-клиенту можно
передать свой requests.Session аргументом session= — ради прокси, адаптеров
или общего пула соединений.
Токен, который становится известен в рантайме (например, канал только что
создан), подставляйте через Clone:
ch, _ := admin.CreateChannel(ctx, notifly.ChannelParams{Name: "deploys"})admin.Clone(notifly.WithChannelToken(ch.Token)).Send(ctx, msg)Настройки Go-клиента неэкспортируемые, поэтому Clone — единственный способ:
присваивание client.ChannelToken = …, которое гналось бы с запросами в полёте
и меняло учётку всем, кто делит этот клиент, просто не компилируется. Клон
переиспользует пул соединений. Печать клиента маскирует оба токена
(apiToken:M...bGh4), так что залогировать его целиком безопасно.
Для эндпоинта, у которого нет своего метода, в Go есть Client.Do(ctx, method, path, body, out, opts...) — тот же транспорт с повторами и разбором ошибок. По
умолчанию он авторизуется API-токеном; AsChannel() переключает на токен
канала, AsEither() берёт любой из двух (API-токен, если заданы оба).
Что ещё умеют SDK
Заголовок раздела «Что ещё умеют SDK»Кроме отправки, вопросов, heartbeat’ов и мониторов оба SDK покрывают роутер вебхуков (правила, история событий и вызов ingest-URL, чтобы проверить правило, не дожидаясь настоящей интеграции), машинные ключи каналов и шары, настройки доставки с проверкой адаптера, источники метрик и пороговые алерты, статус-страницы (включая публичное чтение вообще без токена), почтовые ящики, трекинг ошибок сайта с загрузкой source maps из CI, а также текущего пользователя и разбор дневной квоты.
Приём вебхуков — тоже часть SDK: Go отдаёт готовые http.Handler в подпакете
webhook, Python — функцию verify_signature. Подробности и подводные камни
(реплей, ретраи, разные единицы времени в двух payload’ах) — на странице
доставки.
Go-клиент дополнительно умеет Subscribe — поток сообщений и служебных
событий канала по WebSocket, с keepalive и
переподключением:
err := client.Subscribe(ctx, notifly.Handler{ OnMessage: func(ctx context.Context, m *notifly.SentMessage) { fmt.Println(m.Title, m.Message) }, OnEvent: func(ctx context.Context, e notifly.Event) { // e.Kind: heartbeat:ping, heartbeat:alert, channel:status_changed, … },}, nil) // nil = default *SubscribeOptions; returns when ctx is doneЕму нужен токен канала или шары с правом чтения — API-токен сокет не
принимает. Запасного опроса у Subscribe нет: если сокет недоступен, он
возвращает ErrWebSocketUnavailable. В Python realtime нет, там только опрос.
Чего SDK не покрывают
Заголовок раздела «Чего SDK не покрывают»Управление аккаунтом и админкой (пользователи, устройства, 2FA, sudo, смена
тарифа, пополнение), сами API-токены, окна обслуживания
и тихие часы, AI-ассистент, AI-подбор селектора для
монитора контента и AI-сборка шагов многошагового HTTP и браузерного
сценария, нативные приёмники
POST /integrations/alertmanager и POST /integrations/zabbix.
AI-подсказки, которые SDK всё-таки оборачивают: правило роутера по
записанному событию (SuggestRouterRule / suggest_router_rule), правило
почтового ящика по письму (SuggestEmailInboxRule /
suggest_email_inbox_rule) и разбор ошибки сайта (AnalyzeWebErrorIssue и
GetWebErrorIssueAnalysis / analyze_web_error_issue и
get_web_error_issue_analysis).
Два публичных эндпоинта не обёрнуты намеренно: форма анонимной подписки на
статус-страницу (у неё нет админской стороны — управлять созданным нечем) и
приёмник браузерного сниппета POST /script/{token}, который ждёт ровно ту
форму батча, которую строит сам сниппет. Всё это доступно через
REST API напрямую или через MCP-сервер, у которого
поверхность шире.
GitHub Actions
Заголовок раздела «GitHub Actions»В CI SDK не обязателен: сообщение и heartbeat-пинг — это по одному HTTP-запросу. Токен канала и ping-токен храните в секретах репозитория.
- name: Notify about failure if: failure() env: NOTIFLY_TOKEN: ${{ secrets.NOTIFLY_APP_TOKEN }} run: | jq -n --arg t "CI failed: $GITHUB_REPOSITORY" \ --arg m "$GITHUB_WORKFLOW failed on $GITHUB_REF_NAME: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ '{title: $t, message: $m, priority: 8}' | curl -sS --fail-with-body -X POST https://api.notifly.ru/message \ -H "X-Notifly-Key: $NOTIFLY_TOKEN" \ -H "Content-Type: application/json" -d @-
- name: Heartbeat "deploys are alive" env: NOTIFLY_HB_TOKEN: ${{ secrets.NOTIFLY_HEARTBEAT_TOKEN }} run: curl -fsS -X POST "https://api.notifly.ru/heartbeat/ping/$NOTIFLY_HB_TOKEN"В репозитории Notifly есть и composite-action github-action/ — bash +
curl, без Docker. Входы: token (A… для режима message, H… для
heartbeat), mode (message или heartbeat), message, title,
priority (по умолчанию "5" и отправляется всегда, то есть перекрывает
приоритет канала по умолчанию) и url (по умолчанию https://api.notifly.ru).
Сетевые ошибки, 429 и 5xx повторяются, всего до трёх попыток; вопросов
ask action не умеет. Сообщение он шлёт формой
(application/x-www-form-urlencoded), а не JSON: если такой шаг падает с
400, отправляйте JSON, как в примере выше. И главное — репозиторий закрыт,
поэтому uses: truboter/notifly.ru/github-action@main из чужого репозитория
не разрешится; пользуйтесь шагами выше.
Связанные страницы
Заголовок раздела «Связанные страницы»- API-токены — выпуск ключа, права, скоуп и срок действия.
- REST API — полный справочник эндпоинтов.
- CLI-клиент — то же самое из терминала, без кода.
- MCP-сервер — если вместо кода нужен AI-агент с инструментами.
- Вопросы, Heartbeat, Мониторы — что именно автоматизируют эти вызовы.