CLI
iotpulse is a scriptable command-line client for the IoT Pulse
public API. It is a thin wrapper: every command
maps to one public API endpoint, authenticates with a scoped API key, and can
emit raw JSON for piping into jq or a CI pipeline. It holds no logic of its own
— tenant and scope are resolved server-side from your key.
Install
Section titled “Install”Install the CLI globally from npm:
npm install -g @iotpulse/cliThis puts the iotpulse binary on your PATH. Verify it:
iotpulse --versioniotpulse --helpYou need Node.js 20 or newer.
Develop from source
Section titled “Develop from source”To work on the CLI itself, build it from the platform repository instead of the published package:
cd src/clinpm installnpm run buildnpm linknpm link puts your local build of the iotpulse binary on your PATH.
Authenticate
Section titled “Authenticate”Mint a scoped API key in the admin app under Settings → API Keys (the secret,
format iotp_…, is shown once). The CLI reads the key from the first of these it
finds:
- the
--api-keyflag, - the
IOTPULSE_API_KEYenvironment variable, - the config file
~/.config/iotpulse/config.json.
Store it once with the config command:
iotpulse config set apiKey iotp_your_key_hereiotpulse config get # shows the URL and a masked keyiotpulse config path # prints the config file locationThe key is sent as the X-API-Key header on every request. You never pass a
tenant id — the key is bound to a single tenant, and the API resolves it for you.
Pointing at a different API
Section titled “Pointing at a different API”The base URL defaults to https://api.iotpulse.io. Override it with the
--api-url flag, the IOTPULSE_API_URL environment variable, or
iotpulse config set apiUrl <url>. The request timeout defaults to 30s
(IOTPULSE_API_TIMEOUT_MS).
Output
Section titled “Output”By default, list commands print a compact table and single-resource commands
print indented JSON. Pass the global --json flag to print the API’s exact
response body instead — that is the scriptable contract:
iotpulse devices list # tableiotpulse devices list --json | jq '.[].id' # raw JSON, piped to jqOn an API error, the CLI writes a one-line diagnostic to stderr and exits with a non-zero status, so it composes cleanly in scripts and CI.
Commands
Section titled “Commands”Each command requires the matching scope on your key (see Scopes).
Devices
Section titled “Devices”iotpulse devices listiotpulse devices get <device-id>iotpulse devices update <device-id> --data '{"name":"Boiler A"}' # needs devices:writeDevice groups, thresholds, shifts (read-only)
Section titled “Device groups, thresholds, shifts (read-only)”iotpulse groups listiotpulse groups get <group-id>iotpulse thresholds listiotpulse thresholds get <threshold-id>iotpulse shifts listiotpulse shifts get <shift-id>These resources are read-only in the public API v1, so the CLI exposes only
list and get for them.
Anomalies
Section titled “Anomalies”iotpulse anomalies list --device-id <id> --status open --skip 0 --limit 100iotpulse anomalies get <anomaly-id>iotpulse anomalies acknowledge <anomaly-id> # needs anomalies:writeTickets
Section titled “Tickets”iotpulse tickets listiotpulse tickets get <ticket-id>iotpulse tickets update <ticket-id> --data '{"status":"resolved"}' # needs tickets:writeGateway provisioning
Section titled “Gateway provisioning”Mint an enrollment claim for a gateway and choose the groups it lands in.
--group takes a group id or an exact group name. You can repeat it, and you
can leave it out for a gateway only admins can see. A name is looked up in your
own groups, which needs groups:read.
iotpulse provisioning claims create --name "Dock 3" --group "Dock crew" --expires-in 60 # needs provisioning:writeiotpulse provisioning claims list --status pending # needs provisioning:readiotpulse provisioning claims revoke <claim-id> # needs provisioning:writeThe claim code is printed once, on create. provisioning:write must be granted
explicitly: no wildcard or other scope implies it.
Device self-enrollment
Section titled “Device self-enrollment”iotpulse enroll turns the machine it runs on into a certificate-authenticated
device. It is a reference client for self-enrollment.
These commands take no API key: the claim code is the credential for
enrolling, and the device certificate is the credential for renewing.
IOTPULSE_CLAIM_CODE=PULSE-XXXX-XXXX-XXXX \ iotpulse enroll --hardware-model rpi4 --out /etc/iotpulse # key + CSR, redeem the claimiotpulse enroll check --dir /etc/iotpulse # MQTT connect over mTLS, client id = deviceIdiotpulse enroll renew --dir /etc/iotpulse # no-op until renewAfterUtc; --force renews nowenroll generates an ECDSA P-256 key locally and never sends it. It writes the
key and its credentials to --out:
device.key(mode 0600)device.crt(the leaf certificate)chain.pem(the issuing intermediate;cat device.crt chain.pemis the full chain an MQTT client presents)server-ca.pem(verifies the broker’s certificate)enrollment.json(deviceId, tenantId, brokerUrl, topicPrefix, certExpiresAt, renewAfterUtc)
--fingerprint defaults to an id derived from /etc/machine-id (Linux; pass it
explicitly on macOS and Windows). On Windows the file mode is not enforced, so
restrict the --out directory’s ACL yourself. Re-enrolling
into a directory that already holds an enrollment needs --force. The current
certificate is then presented as proof of possession.
renew replaces the leaf and its chain together on every renewal. It
refuses to store a renewal that did not return a usable chain. A successful
renewal revokes the certificate it was made with, so if a renewal times out,
don’t assume it failed: retry with --force.
Device certificates
Section titled “Device certificates”List, issue, revoke and audit device client certificates. The
device-certificates:read, device-certificates:issue and
device-certificates:revoke scopes must each be granted explicitly; none implies
another.
iotpulse certs list --device-id gw-1 --status active # needs device-certificates:readiotpulse certs issue gw-1 --out ./certs # needs device-certificates:issueiotpulse certs issue gw-1 --out ./certs --p12 # also writes gw-1.p12 + gw-1.p12.passiotpulse certs revoke <certificate-id> # needs device-certificates:revokeiotpulse certs audit # needs device-certificates:readcerts issue gets the device’s private key exactly once. It writes
<device>.key.pem, .crt.pem, .chain.pem and .server-ca-bundle.pem into
--out. The server CA bundle lets the device verify the broker
(messaging.iotpulse.io:8883). Don’t confuse it with .chain.pem, which is
your client certificate’s chain. The files are owner-only (0600), and the
directory is created 0700 if it doesn’t exist. On Windows those modes aren’t
enforced, and the files inherit the directory’s ACL, so pick a directory only
you can read.
The command creates every file, empty, before it issues anything. So if a
file already exists, or --out isn’t writable, the command stops before a
certificate is minted, and you never lose a key or use up one of the device’s
certificate slots. Existing files are never overwritten. If the request fails,
the empty files are removed again.
The key is never printed. That includes --json, which returns the bundle
without the key, PKCS#12 or passphrase. If you want the whole bundle on stdout
(to pipe into a secret store, say), pass --stdout. It then goes wherever your
terminal or CI logs keep output. With both --out and --stdout, a failed save
still prints the bundle.
If the device already holds a self-enrolled certificate, the API answers
409 device_self_enrolled. Add --acknowledge-self-enrollment-override to
issue anyway. If the device is at its certificate limit
(409 device_cert_cap_reached), the error tells you how to list the active
certificates and revoke one.
If the connection drops or times out mid-request, the certificate may still
have been issued. Check iotpulse certs list --device-id <id> --status active
and revoke any certificate you don’t hold a key for.
Request bodies (--data)
Section titled “Request bodies (--data)”Write commands (devices update, tickets update) take a --data / -d flag
with the JSON body. It accepts a raw string, a file, or stdin:
iotpulse devices update dev_1 --data '{"name":"Boiler A"}'iotpulse devices update dev_1 --data @device.jsoncat device.json | iotpulse devices update dev_1 --data -The body is forwarded verbatim — the API validates it and returns a structured error if it is malformed.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 |
Success. |
1 |
API returned an error (4xx/5xx) or the request failed (network/timeout). |
2 |
Usage error, including no API key configured. |
Scripting example
Section titled “Scripting example”# Acknowledge every open anomaly for a device.iotpulse anomalies list --device-id dev_123 --status open --json \ | jq -r '.[].id' \ | while read -r id; do iotpulse anomalies acknowledge "$id"; done