Skip to content

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.

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 cli
go build -o notifly cli.go
./notifly version

make 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/notifly
notifly init # setup wizard (writes the config)
notifly version # show the version (alias: v)
notifly config # show the current config
notifly push # send a message (alias: p)
notifly watch # watch a command's output and send the changes
notifly 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 answer
notifly help # help on the commands (alias: h)

Help for a specific command: notifly help push, notifly help watch.

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 init
Notifly URL: https://api.notifly.ru
Notifly v2.7.4@2025-09-25-16:27:04
Configure an application token
1. Enter an application-token
2. 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.com
Password:
Signing in -> Success!
Application name: deploy-bot
Application description (can be empty): CI notifications
Creating -> 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.json

At 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 write access, no channel scope and no expiry, named notifly-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.

init will offer to choose a path, and other commands search for the config in these locations (the first found wins):

  1. ./cli.json (next to the current directory);
  2. $XDG_CONFIG_HOME/notifly/cli.json (usually ~/.config/notifly/cli.json);
  3. ~/.notifly/cli.json;
  4. /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.

The config is just JSON. You can create it manually without running the wizard:

{
"token": "AbC3dEf5gHi7jKl9mNo1pQr",
"url": "https://api.notifly.ru",
"defaultPriority": 6
}
FieldDescriptionExample
tokenApp token (an API token or a device token will not work)AbC3dEf5gHi7jKl9mNo1pQr
urlNotifly API URLhttps://api.notifly.ru
defaultPriorityDefault 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 initMx7Kp2Lq9Wz4Rt6Yb1Nd8Fs

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 created
ID: 42
Name: deploy-bot
App-Token: AbC3dEf5gHi7jKl9mNo1pQr
$ notifly list -n 10

create-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 config
Used Config: /home/user/.config/notifly/cli.json
URL: https://api.notifly.ru
Default Priority: 6
Token: AbC3dEf5gHi7jKl9mNo1pQr
API Token: set (M…d8Fs)

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: yes

Flags go before the question text: anything after it is taken as part of the question.

FlagPurposeDefault
-o, --optionsComma-separated answers; without them the answer is free text—
--timeoutSeconds until the question expiresno expiry
--default-answerAnswer counted on expiry (needs --timeout)—
-t, --titleNotification titlechannel name
-w, --waitWait for the answer instead of printing just the idoff
--pollFallback polling interval while waiting, in seconds3
-q, --quietPrint only the answer — handy for $(...)off
--token, --urlOverride 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.sh
fi

The message text can be provided as an argument or via stdin:

Окно терминала
notifly push my message
notifly push "my message"
echo "my message" | notifly push
notifly push < somefile

Title and priority:

Окно терминала
notifly push -t "Deploy" -p 10 "Production deployed"

Flags for the push command:

FlagPurposeDefault
-p, --priorityMessage priorityfrom defaultPriority
-t, --titleTitle (if empty — application name is used)application name
--tokenOverride App token (env: NOTIFLY_TOKEN, GOTIFY_TOKEN)from config
--urlOverride server URLfrom config
-q, --quietPrint nothing on successoff
--contentTypeMessage content-type, e.g. text/markdown (see msgextras)—
--clickUrlURL opened when the notification is clicked (see msgextras)—
--bigImageUrlURL of an image shown in the notification (see msgextras)—
--disable-unescape-backslashDo 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.

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"

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)"
Окно терминала
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.

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:

FlagPurposeDefault
-n, --intervalPolling interval in seconds (fractional allowed)2
-p, --priorityPriorityfrom defaultPriority
-x, --execWhat to use to execute the commandsh -c
-t, --titleTitle (if empty — the command text is used)command
--tokenOverride App token (env: NOTIFLY_TOKEN)from config
--urlOverride server URLfrom config
-o, --outputDetail level of the message body: short, default, longdefault

--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"

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 certificate
export SSL_CERT_FILE=/path/to/cert.pem

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"}'

The CLI works over the same public API as everything else. Authorization — the X-Notifly-Key header with an App token (A…).

MethodPathPurposeToken
POST/messageSend a message to the channelApp token
GET/versionServer 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.