CLI client
Notifly-CLI is a small command-line utility notifly for sending
messages to your channel directly from the terminal, cron jobs, and shell scripts.
It stores the token and server URL in a config, can read the message from
stdin, and can watch the output of an arbitrary command (watch) and send updates.
The utility is not required to send notifications — the same can
be done with curl (see REST API). The CLI simply saves you
from repeating the token and URL in every command.
Installation
Section titled “Installation”There are no prebuilt CLI downloads yet: the utility is built from source — the
cli/ directory of the Notifly repository (for access to the sources, email
support@notifly.ru). You need Go 1.27+ and the
sdk/go directory of the same repository next to it: the CLI uses the Go SDK
from the working tree.
cd cligo build -o notifly cli.go./notifly versionmake build builds a set of binaries into the build/ subdirectory at once —
for Linux (amd64, 386, arm64, armv7), macOS (amd64, arm64) and Windows (amd64,
386); the files keep the old names gotify-cli-<os>-<arch>, a legacy of the
fork.
If you wish, place the binary in a directory on your $PATH:
sudo mv notifly /usr/local/bin/notiflyCommands
Section titled “Commands”notifly init # setup wizard (writes the config)notifly version # show the version (alias: v)notifly config # show the current confignotifly push # send a message (alias: p)notifly watch # watch a command's output and send the changesnotifly create-app # create a channel (needs an API token)notifly list # latest messages (alias: ls; needs an API token)notifly ask # ask a question and optionally wait for the answernotifly help # help on the commands (alias: h)Help for a specific command: notifly help push, notifly help watch.
Setup: notifly init
Section titled “Setup: notifly init”The easiest way to get started is the init wizard. It will ask for the server URL,
token and default priority, check the connection and write the config.
$ notifly initNotifly URL: https://api.notifly.ruNotifly v2.7.4@2025-09-25-16:27:04
Configure an application token1. Enter an application-token2. Create an application token (with user/pass)Enter 1 or 2: 2
Sign in (credentials are used now and not saved; only the issued tokens are)Email: me@example.comPassword:Signing in -> Success!
Application name: deploy-botApplication description (can be empty): CI notificationsCreating -> Success!
Default Priority [0-10]: 6
API token for the account-level commands (create-app, list)1. Enter an API token (issued on the "API tokens" page of the admin UI)2. Issue a new API token (with user/pass)Press Enter to skip — pushing works without one.Enter 1, 2 or nothing: 2
API token name [notifly-cli (myhost)]:Issuing API token -> Success!Issued API token "notifly-cli (myhost)" (M…d8Fs); manage it on the "API tokens" page of the admin UI.
Written config to: /home/user/.config/notifly/cli.jsonAt the App token step there are two options:
- 1 — enter an existing App token. Get it from the admin UI; the utility verifies it right away by sending a test message to the channel.
- 2 — create an App token with your email/password. The CLI signs in as a
temporary device named
notifly-cli setup (hostname), creates a channel with the given name and description and saves its token. With 2FA enabled the wizard asks for a code from your authenticator app or a recovery code; with the “Notifly mobile” method you can approve the sign-in in the app and just press Enter. The email and password are not saved — only the resulting token is.
The default priority must be a number from 0 to 10.
The last step is the API token for create-app and
list. Enter skips it: pushing works without one.
- 1 — paste an existing API token from the API tokens page of the admin UI; the wizard checks it with a single read request.
- 2 — issue a new API token with your email/password. The same sign-in as
for the App token: if you signed in on the previous step, the password is not
asked again. The token is issued with
writeaccess, no channel scope and no expiry, namednotifly-cli (hostname); narrow the scope or set an expiry later on the API tokens page.
The wizard deletes the temporary device before writing the config, so neither a
device slot nor a device token is left behind. Like any password sign-in it
sends the owner a “new sign-in” email, and on a full device limit it suspends
the oldest device for a while (it wakes up on its next request). If the wizard
is interrupted, the device notifly-cli setup (…) stays on the Devices
page — delete it by hand.
The config file is created with mode 0600: it holds live tokens.
Where the config is stored
Section titled “Where the config is stored”init will offer to choose a path, and other commands search for the config in these
locations (the first found wins):
./cli.json(next to the current directory);$XDG_CONFIG_HOME/notifly/cli.json(usually~/.config/notifly/cli.json);~/.notifly/cli.json;/etc/notifly/cli.json(except on Windows).
If none of those exist, the same paths with gotify instead of notifly are
tried next — a leftover of the fork, so an old config keeps working.
Manual config
Section titled “Manual config”The config is just JSON. You can create it manually without running the wizard:
{ "token": "AbC3dEf5gHi7jKl9mNo1pQr", "url": "https://api.notifly.ru", "defaultPriority": 6}| Field | Description | Example |
|---|---|---|
token | App token (an API token or a device token will not work) | AbC3dEf5gHi7jKl9mNo1pQr |
url | Notifly API URL | https://api.notifly.ru |
defaultPriority | Default priority (0 if not set) | 6 |
apiToken | (optional) API token (M…) for create-app and list; paste one from the admin UI or issue it in notifly init | Mx7Kp2Lq9Wz4Rt6Yb1Nd8Fs |
Account-level commands: create-app and list
Section titled “Account-level commands: create-app and list”Besides sending, the CLI can do two things to the account itself — create a
channel and read the latest messages. Those need an
API token (M…) rather than an app token: issue it on the
API tokens page of the admin UI or right in the notifly init wizard with
your email and password. The CLI picks it up from the apiToken field, the
--apiToken flag or the NOTIFLY_API_TOKEN environment variable.
$ export NOTIFLY_API_TOKEN=Mx7Kp2Lq9Wz4Rt6Yb1Nd8Fs
$ notifly create-app -n "deploy-bot" -d "CI notifications"Channel createdID: 42Name: deploy-botApp-Token: AbC3dEf5gHi7jKl9mNo1pQr
$ notifly list -n 10create-app prints the app token of the new channel — that is what goes into
token, so that notifly push can send to that channel afterwards. A channel
created by an API token with a limited scope lands in that scope automatically.
notifly init offers the API token as a separate step: paste an existing one or
issue a new one with your email and password (see Setup).
Press Enter to skip it if you do not need the account-level commands.
To see which config is being used now:
$ notifly configUsed Config: /home/user/.config/notifly/cli.jsonURL: https://api.notifly.ruDefault Priority: 6Token: AbC3dEf5gHi7jKl9mNo1pQrAPI Token: set (M…d8Fs)Asking a human: notifly ask
Section titled “Asking a human: notifly ask”notifly ask puts a question through a channel — the notification arrives with
answer buttons, and the CLI can wait for one to be pressed. That is a ready-made
approval gate for a deployment script; the mechanics themselves are on the
Questions (Ask) page. It needs the channel’s app token, the
same one push uses.
$ notifly ask -o yes,no --wait "Deploy to production?"question created, id: 8260183728527568 — waiting for the answer (Ctrl+C to stop)...answer: yesFlags go before the question text: anything after it is taken as part of the question.
| Flag | Purpose | Default |
|---|---|---|
-o, --options | Comma-separated answers; without them the answer is free text | — |
--timeout | Seconds until the question expires | no expiry |
--default-answer | Answer counted on expiry (needs --timeout) | — |
-t, --title | Notification title | channel name |
-w, --wait | Wait for the answer instead of printing just the id | off |
--poll | Fallback polling interval while waiting, in seconds | 3 |
-q, --quiet | Print only the answer — handy for $(...) | off |
--token, --url | Override the app token and address (env: NOTIFLY_TOKEN) | from config |
With --wait the answer arrives as a WebSocket push, so the command returns the
moment someone presses a button. When the socket is unavailable the CLI falls
back to polling every --poll seconds. Ctrl+C stops the wait; the question
itself stays unanswered.
The exit codes make the command usable in an if: 0 — an answer arrived,
1 — the question expired, was cancelled, or the wait was interrupted.
if answer=$(notifly ask -q -o yes,no --wait "Deploy to production?") && [ "$answer" = yes ]; then ./deploy.shfiSending messages: notifly push
Section titled “Sending messages: notifly push”The message text can be provided as an argument or via stdin:
notifly push my messagenotifly push "my message"echo "my message" | notifly pushnotifly push < somefileTitle and priority:
notifly push -t "Deploy" -p 10 "Production deployed"Flags for the push command:
| Flag | Purpose | Default |
|---|---|---|
-p, --priority | Message priority | from defaultPriority |
-t, --title | Title (if empty — application name is used) | application name |
--token | Override App token (env: NOTIFLY_TOKEN, GOTIFY_TOKEN) | from config |
--url | Override server URL | from config |
-q, --quiet | Print nothing on success | off |
--contentType | Message content-type, e.g. text/markdown (see msgextras) | — |
--clickUrl | URL opened when the notification is clicked (see msgextras) | — |
--bigImageUrl | URL of an image shown in the notification (see msgextras) | — |
--disable-unescape-backslash | Do not unescape \n and \t (leave them as literal text) | unescapes |
If -p is not specified, defaultPriority from the config is used; if that is absent —
priority 0 is used.
Overriding token and URL
Section titled “Overriding token and URL”You don’t have to keep a config — you can pass the token and URL directly on the command
line or via the NOTIFLY_TOKEN environment variable (the old GOTIFY_TOKEN
is still accepted):
notifly push --url https://api.notifly.ru --token AbC3dEf5gHi7jKl9mNo1pQr "without config"
NOTIFLY_TOKEN=AbC3dEf5gHi7jKl9mNo1pQr notifly push --url https://api.notifly.ru "from env"Markdown and line breaks
Section titled “Markdown and line breaks”By default notifly unescapes \n and \t into real line breaks and tabs.
This is convenient for multi-line messages provided as a single-line argument:
notifly push -t "Report" "Line 1\nLine 2\twith tab"To keep \n and \t as literal text — add --disable-unescape-backslash.
Markdown is enabled via --contentType:
notifly push --contentType text/markdown -t "Release" "**v2.0** is ready — [changelog](https://notifly.ru)"Clickable notifications and images
Section titled “Clickable notifications and images”notifly push -t "Build failed" \ --clickUrl https://ci.example.com/build/42 \ --bigImageUrl https://ci.example.com/build/42/graph.png \ "Job #42 finished with an error"More about these fields — see the Message extras page.
Watching a command: notifly watch
Section titled “Watching a command: notifly watch”watch periodically runs the given command and as soon as its output
has changed compared to the previous run, sends the new output to the channel.
Useful for simple ad-hoc monitoring without a separate service.
notifly watch "curl -s https://api.example.com/status | jq '.state'"By default the command runs every 2 seconds via sh -c. The title, if not set,
defaults to the command itself.
Flags for the watch command:
| Flag | Purpose | Default |
|---|---|---|
-n, --interval | Polling interval in seconds (fractional allowed) | 2 |
-p, --priority | Priority | from defaultPriority |
-x, --exec | What to use to execute the command | sh -c |
-t, --title | Title (if empty — the command text is used) | command |
--token | Override App token (env: NOTIFLY_TOKEN) | from config |
--url | Override server URL | from config |
-o, --output | Detail level of the message body: short, default, long | default |
--output modes:
short— only the new output, without wrappers;default— the new output wrapped in== BEGIN NEW OUTPUT == … == END NEW OUTPUT ==;long— both old and new output (full diff in OLD/NEW blocks).
Example: watch the number of containers every 30 seconds and send a long diff:
notifly watch -n 30 -o long -t "docker ps" "docker ps -q | wc -l"TLS and self-signed certificates
Section titled “TLS and self-signed certificates”If the server uses a self-signed certificate, there are environment variables:
# completely disable TLS verification (insecure)export NOTIFLY_SKIP_VERIFY_TLS=True
# or specify a custom root CA / pinned certificateexport SSL_CERT_FILE=/path/to/cert.pemAlternative without the CLI: curl
Section titled “Alternative without the CLI: curl”Any send performed by the CLI can be reproduced with plain curl — it’s the same REST API:
curl -X POST "https://api.notifly.ru/message" \ -H "X-Notifly-Key: AbC3dEf5gHi7jKl9mNo1pQr" \ -H "Content-Type: application/json" \ -d '{"title": "Deploy", "priority": 10, "message": "Production is live"}'REST API
Section titled “REST API”The CLI works over the same public API as everything else.
Authorization — the X-Notifly-Key header with an App token (A…).
| Method | Path | Purpose | Token |
|---|---|---|---|
POST | /message | Send a message to the channel | App token |
GET | /version | Server version (used by the init wizard) | — |
Body for POST /message (JSON): message (required), title,
priority (0–10), extras (for example client::display.contentType,
client::notification.click.url, client::notification.bigImageUrl).
Details — on the Sending messages and
Message extras pages.