axis-cli
Tutorial · v1.1.2 · about 15 minutes

axis-cli 101

From nothing installed to running real commands against your camera. We use an AXIS Q1656 as the example, but every step is identical for any Axis camera.

No prior command-line experience assumed. If you already have axis installed, skip to step 3.

First, what is this thing?

axis-cli is a program you run by typing commands. It talks to your camera over the network using VAPIX — the same HTTP API the camera's own web interface uses. Anything you can click in the camera's web page, you can script here instead.

The one idea that makes it pleasant: you save each camera once as a named profile, then refer to it by that name forever. No retyping IP addresses, no retyping passwords.

It is not a daemon

Nothing runs in the background. You type a command, it talks to the camera, prints the answer, and exits.

It runs on your computer

Not on the camera. Install it on your Mac or PC; it reaches cameras over your LAN or via CamStreamer Cloud.

Credentials are saved once

Profiles live in ~/.axis-cli/config.json, permissions 0600. They survive reboots.

1

Open a terminal

This is the window where you type commands. You'll keep it open for the rest of this tutorial.

  • macOS — press ⌘ Space, type Terminal, press Return.
  • Windows — press the Windows key, type Terminal (or PowerShell), press Enter.

You'll see a line ending in $, %, or >. That's the prompt — it means the terminal is waiting for you. In the examples below, that leading symbol is not something you type.

One habit worth forming

Don't paste a command that has a # comment on the end of the line. In macOS's default zsh shell, interactive comments are off, so the comment gets passed to the program as arguments and you get a confusing error. Copy the command only.

2

Install axis-cli

You need Node.js 18 or newer. The installer scripts check for it and tell you what to do if it's missing.

  1. Download axis-cli-v1.1.2-installer.zip from the latest release.
  2. Unzip it. You get a folder called axis-cli.
  3. Double-click install.command inside that folder.
macOS will block it the first time

The script isn't signed by an Apple developer account, so Gatekeeper refuses a plain double-click. Right-click install.command → Open, then confirm. You only do this once. If Finder won't run it at all, open Terminal and run:

cd ~/Downloads/axis-cli
chmod +x install.command
./install.command

The installer checks Node, installs dependencies, builds, installs the axis command globally, and verifies it. Expect output like:

==> Checking Node.js
  ok Node v22.14.0  (/opt/homebrew/bin/node)
  ok npm 10.9.2

==> Installing dependencies
  ok Dependencies ready

==> Building TypeScript
  ok Built dist/index.js

==> Installing 'axis' globally
  ok Installed into /opt/homebrew

==> Verifying
  ok axis 1.1.2  (/opt/homebrew/bin/axis)

Check it worked

axis --version
1.1.2

That's permanent. You do not reinstall after a reboot — the axis command lives in Node's global folder on your system drive and stays on your PATH.

One exception: nvm

If your Node came from nvm, a global install belongs to that one Node version. Switch versions and axis seems to vanish — just re-run the installer. Installing Node via Homebrew (brew install node) sidesteps this.

3

Find your Q1656 on the network

You need its IP address. Pick whichever you already have to hand:

  • AXIS IP Utility or AXIS Device Manager — lists every Axis device on the subnet with model and IP. Easiest if you have it installed.
  • Your router's DHCP client list — look for a hostname like axis-b8a44f… or a MAC starting 00:40:8C (Axis Communications).
  • The camera's own web page — if you already know the address, the IP is right there in the browser bar.
  • From the terminal — after pinging the subnet, Axis devices show up by their MAC prefix:
    # macOS / Linux
    arp -a | grep -i "0:40:8c"
    
    # Windows
    arp -a | findstr "00-40-8c"

For the rest of this tutorial the Q1656 is at 192.168.1.50. Substitute your own address everywhere you see it.

Sanity check before you go further

Open http://192.168.1.50 in a browser and log in. If that doesn't work, axis-cli won't either — it's the same network path and the same credentials. Fix it here first.

4

Add the camera

This is the step you were asking about — and the answer to "where does it ask for the username and password?" is: right here, once. Run:

The command
axis camera add q1656 --ip 192.168.1.50 --user root

Notice there is no password on that line. Because you left --pass off, the CLI stops and asks you:

What you see
Password for root@192.168.1.50: 

Type the camera's password and press Return. Nothing appears as you type — no dots, no asterisks, no movement at all. That's deliberate, not a frozen terminal. Then:

   Saved camera profile "q1656" to /Users/you/.axis-cli/config.json
  → Verify it with: axis camera test q1656
That's the last time you type it

The password is stored in ~/.axis-cli/config.json (Windows: %USERPROFILE%\.axis-cli\config.json), file mode 0600 — readable only by your user account. Every later command just takes the profile name q1656.

What each part means

q1656
The profile name — your label, anything you like. lobby, gate-north, q1656. This is what you'll type in every other command.
--ip 192.168.1.50
The camera's address. A hostname works too. Note it's --ip, not --host.
--user root
The camera account name. root is the default, so you can omit this flag entirely — it's shown here to make the point that the username is a flag, not a prompt.
--pass (omitted on purpose)
Leave it off to get the hidden prompt. See below for when you'd use it.

Three ways to supply the password

The CLI checks these in order and uses the first one it finds.

MethodHowWhen to use it
1. Hidden prompt Omit --pass Default choice. Nothing lands in shell history or in ps output.
2. Environment variable AXIS_PASS Scripts, CI, cron — anywhere there's no terminal to prompt at.
3. Flag --pass hunter2 Last resort. The password is now in your shell history and visible to other processes.
Method 2 — for scripts
# macOS / Linux
export AXIS_PASS='your-password'
axis camera add q1656 --ip 192.168.1.50

# Windows PowerShell
$env:AXIS_PASS = 'your-password'
axis camera add q1656 --ip 192.168.1.50
Q1656 running AXIS OS 11 or 12?

Since AXIS OS 11 there is no factory default password — you set one on first boot. If you've never logged into this camera, do that in the web interface first; there's no password for axis-cli to use until you have.

Better practice: don't use root

Create a dedicated account in the camera's web UI (System → Users) and use that instead. Administrator role is needed for param set, reboot, and starting/stopping ACAPs; Operator is enough for PTZ and read-only queries.

axis camera add q1656 --ip 192.168.1.50 --user cli-svc

Fixing or changing a profile

Change the stored password (bare --pass prompts again, hidden)
axis camera update q1656 --pass

Camera moved to a new address
axis camera update q1656 --ip 192.168.1.77

Wrong username
axis camera update q1656 --user cli-svc --pass

Start over
axis camera remove q1656
5

Verify it works

Always do this before anything else. It proves the network path and the credentials in one shot.

axis camera test q1656
   "q1656" reachable — AXIS Q1656 Box Camera (firmware 11.11.61)

If that came back green, you're done setting up. Confirm what you've saved:

axis camera list
┌───────┬────────────────────┬──────┬─────┐
│ Name  │ Access             │ User │ TLS │
├───────┼────────────────────┼──────┼─────┤
│ q1656 │ 192.168.1.50:80    │ root │ no  │
└───────┴────────────────────┴──────┴─────┘

Passwords are never printed, not even with --json.

6

Run your first real commands

Every command follows the same shape: axis <area> <action> <profile>.

See what the camera is

axis info q1656
axis info q1656 --all-acaps   also lists third-party ACAPs

Brand, model, firmware, serial, and which CamStreamer-family ACAPs are installed and running.

Read and write settings

axis param get q1656 root.Brand.ProdFullName root.Network.eth0.IPAddress
axis param set q1656 root.Time.DST=yes

These are the same parameters you'd find in the camera's plain-config page.

ACAP applications

axis apps list q1656
axis apps list q1656 --running
axis apps restart q1656 CamScripter

Watch live events

First, see what this camera actually publishes:

axis events topics q1656
┌─────────────────────────────────────────────────────────┐
│ Topic filter                                            │
├─────────────────────────────────────────────────────────┤
│ tns1:Device/tnsaxis:IO/VirtualPort                      │
├─────────────────────────────────────────────────────────┤
│ tns1:VideoSource/tnsaxis:DayNightVision                 │
├─────────────────────────────────────────────────────────┤
│ tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY │
└─────────────────────────────────────────────────────────┘

Then stream them. With no --topic, you get everything:

axis events watch q1656
2026-07-29T09:14:02.199Z  tns1:Device/tnsaxis:IO/VirtualPort  port=1 state=1
2026-07-29T09:14:05.400Z  tns1:Device/tnsaxis:IO/VirtualPort  port=1 state=0

Or narrow it down — --topic is repeatable:

axis events watch q1656 --topic 'tns1:Device/tnsaxis:IO/VirtualPort'
axis events watch q1656 --topic 'tns1:Device//.' --topic 'tns1:VideoSource//.'
axis events watch q1656 --raw   full JSON payload per event

Press Ctrl+C to stop — that's how you exit any long-running command.

Topic filters are ONVIF topic expressions

They're the exact strings the camera declares, which is why axis events topics exists — copy a value from there rather than guessing. A trailing //. matches everything below a branch, so tns1:Device//. covers every device event.

Axis recommends subscribing only to what you need: each subscription starts internal services on the camera, and subscribing to everything on a busy device can affect performance.

CamOverlay, CamStreamer, CamSwitcher

CamOverlay — toggle a graphic, push text into a field
axis overlay list q1656
axis overlay text q1656 2 temperature=22.5C

CamStreamer — start/stop a configured stream
axis stream list q1656
axis stream start q1656 42448
axis stream stop q1656 42448

CamSwitcher — change the live view
axis switcher switch q1656 MainView
Stopping a stream can take longer than starting one

stream start just flips a flag and returns immediately. stream stop waits for the camera to gracefully close its connection to the RTMP/HLS/SRT destination first, which can take longer — the CLI allows up to 20 seconds by default (10 for start). If you still see "The operation was aborted due to timeout", add --timeout 30000 rather than assuming something is broken.

Anything not wrapped yet

axis vapix get q1656 /axis-cgi/param.cgi action=list group=Image
When you get stuck on syntax

axis --help lists every area. axis camera --help or axis overlay text --help drills into one. Misspell a command and it suggests the nearest match.

7

Add the rest of your fleet

Repeat step 4 for each camera, giving each a memorable name:

axis camera add lobby     --ip 192.168.1.51
axis camera add gate-north --ip 192.168.1.52
axis camera add warehouse  --ip 192.168.1.53

Now one command checks all of them, probed in parallel so a dead camera doesn't stall the report:

axis fleet health

Reachability, firmware, SD card status, and how many CamStreamer-family ACAPs are running. It exits with code 1 if any camera is unreachable, which is what makes it usable as a monitoring check rather than just a pretty table.

Cameras behind CamStreamer Cloud

No local IP? Use a device-connect.net URL and a DEVICE_ACCESS_TOKEN instead:

axis camera add remote-cam --cloud-url https://xxxx.device-connect.net

You'll be prompted for the token, hidden, exactly like a password (or set AXIS_CLOUD_TOKEN). Note that axis events watch needs a local --ip profile, since it opens a direct WebSocket to the camera.

Machine-readable output

Add --json anywhere on the line to get JSON instead of tables:

axis fleet health --json | jq '.[] | select(.reachable == false)'
8

Switch to HTTPS (optional)

Plain HTTP sends credentials over your LAN in the clear. If the camera has HTTPS enabled, add --tls. Port then defaults to 443 instead of 80.

axis camera add q1656 --ip 192.168.1.50 --tls --tls-insecure

--tls-insecure accepts a self-signed certificate. Most Axis cameras ship with one, so you usually need it — the connection is still encrypted, it just isn't verified against a CA. Drop the flag once you've installed a proper certificate.

Switch an existing profile — the port moves to 443 with it
axis camera update q1656 --tls --tls-insecure
axis camera test q1656

Troubleshooting

The failures people actually hit, and what each one really means.

command not found: axis

The install worked but the folder holding axis isn't on your PATH. Find out where it went:

npm prefix -g

Then add that folder's bin to your shell profile, once:

echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

On Windows, open a new terminal first — PATH changes don't reach windows that were already open.

axis worked yesterday and is gone today

Two usual causes:

  • You used npm link and the project folder moved — or lives on an external drive that isn't mounted. Reinstall with npm install -g ., which copies rather than symlinks.
  • nvm switched Node versions. Global installs belong to one Node version. Re-run the installer, or move to Homebrew Node.
zsh: permission denied: axis

The compiled entry point lost its executable bit — tsc doesn't set one, so anything that regenerates dist/ leaves it mode 644. Re-run npm run build (the postbuild step fixes the mode), or set it directly:

chmod +x dist/index.js
sh: tsc: command not found during the build

You have NODE_ENV=production set, so npm install skipped devDependencies and TypeScript was never installed. Force them in:

npm install --include=dev
camera test fails with 401 Unauthorized

Reached the camera, credentials rejected. In order of likelihood:

  • Wrong password. Retype it: axis camera update q1656 --pass
  • Wrong username — --user defaults to root, which may not be the account you meant. axis camera list shows what's stored.
  • The account exists but lacks the privilege for that operation. Reads work, writes need Administrator.
  • A password with $, ! or " passed via --pass got mangled by your shell. Use the hidden prompt instead — it never touches shell quoting.
camera test times out or can't connect

Network, not credentials. Work outward:

  • Open http://192.168.1.50 in a browser. If that fails too, it's not axis-cli.
  • Different VLAN or subnet? You need a route to the camera.
  • Camera set to HTTPS only? Then plain HTTP is refused — add --tls --tls-insecure.
  • Non-standard port: axis camera update q1656 --port 8080
  • Did the IP change? DHCP leases move. Check with AXIS IP Utility.
TLS error: self-signed certificate

Expected on a stock Axis camera. Add --tls-insecure to accept it. Traffic stays encrypted; only CA verification is skipped.

axis camera update q1656 --tls-insecure
events watch: "Could not use supplied event filter"

The camera rejected a topic filter. On axis-cli 1.1.0 this happened on every invocation — a bug: the CLI asked the camera to subscribe to a topic literally called event. Upgrade to 1.1.1 or later.

On 1.1.1+, it means the filter you passed isn't one the camera accepts. Find the real ones:

axis events topics q1656
axis events watch q1656 --topic 'tns1:Device/tnsaxis:IO/VirtualPort'

Watch your shell quoting too — a filter containing // or : should be wrapped in single quotes.

events watch can't connect over HTTPS

On 1.1.0, events watch silently ignored --tls-insecure, so a --tls profile using the camera's stock self-signed certificate failed to open the WebSocket. Fixed in 1.1.1. Also note that event watching needs a local --ip profile — a CamStreamer Cloud profile can't open a direct WebSocket to the camera.

stream stop: "The operation was aborted due to timeout"

Not a broken stream — the camera hadn't finished tearing down its connection to the streaming destination within the wait window. stream stop waits 20 seconds by default; stream start only 10, since starting returns immediately while stopping an actively-publishing stream has real teardown work to do.

If 20 seconds isn't enough for this camera or destination, raise it:

axis stream stop q1656 42448 --timeout 30000

Check axis stream show q1656 42448 afterwards — it usually reports active: false even after a timeout, meaning the stop went through on the camera side and only the CLI gave up waiting for the confirmation.

The password prompt never appears

The prompt needs a real terminal. Inside a script, a pipe, or a CI job there's no TTY, so the CLI refuses to prompt and errors instead. Set AXIS_PASS in those contexts.

npm error EINVALIDTAGNAME ... Invalid tag name "#"

You pasted a command with a trailing # comment into interactive zsh. Unlike bash, zsh doesn't treat # as a comment on the command line, so it becomes an argument. Backticks inside such a comment are worse — zsh executes them. Paste the command without the comment, or enable comments for the session with setopt interactive_comments.

Where is my config, and how do I start clean?

~/.axis-cli/config.json on macOS/Linux, %USERPROFILE%\.axis-cli\config.json on Windows. Mode 0600. Delete the file to wipe every profile. Uninstalling the CLI leaves it alone:

npm uninstall -g axis-cli

Credentials are stored in plaintext, so treat the file as a secret and never commit it anywhere.

Cheat sheet

Every command takes the profile name, never a raw IP.

GoalCommand
Save a cameraaxis camera add q1656 --ip 192.168.1.50
Prove it worksaxis camera test q1656
List profilesaxis camera list
Change passwordaxis camera update q1656 --pass
Delete a profileaxis camera remove q1656
Model & firmwareaxis info q1656
Read a parameteraxis param get q1656 root.Brand.ProdFullName
Write a parameteraxis param set q1656 root.Time.DST=yes
Go to a PTZ presetaxis ptz goto q1656 "Home" -c 1
List ACAPsaxis apps list q1656
Restart an ACAPaxis apps restart q1656 CamScripter
Toggle an overlayaxis overlay enable q1656 2
Push overlay textaxis overlay text q1656 2 temp=22.5C
Start a streamaxis stream start q1656 42448
Stop a streamaxis stream stop q1656 42448 --timeout 30000
Switch the viewaxis switcher switch q1656 MainView
Check every cameraaxis fleet health
List event topicsaxis events topics q1656
Watch live eventsaxis events watch q1656
Watch one topicaxis events watch q1656 --topic 'tns1:Device//.'
Rebootaxis reboot q1656 --yes
Raw VAPIX callaxis vapix get q1656 /axis-cgi/param.cgi action=list
JSON outputappend --json to anything
Helpaxis --help, axis <area> --help
Version notes

1.1.2 fixes axis stream stop timing out on actively-publishing streams and adds --timeout to stream start/ stream stop. 1.1.1 repairs axis events watch, which failed on every invocation in 1.1.0, and adds axis events topics. 1.1.0 changed two behaviours from 1.0.0: --tls now defaults to port 443 (was 80), and axis reboot without --yes refuses and exits 1. See the changelog.