Open-source shell scripts and copy-pasteable curl recipes for the
ACS Monitor REST API — the network and
infrastructure monitoring platform from
Anglia Computer Solutions.
Pure shell. Useful for one-shot automation, cron jobs, ad-hoc poking
around with curl, runbook snippets, ChatOps, and copy-pasting into
Slack threads.
Licensing: these shell examples are open source under the MIT licence. The ACS Monitor application they talk to is licensed commercial software with a free 100-device tier — see acsmon.com for tiers and pricing.
- Requirements
- Quick start
- Environment variables
- Provided scripts
- Authentication recipes
- Devices (SNMP) recipes
- Service-monitor recipes
- Alert-event recipes
- System health recipes
- Pagination patterns
- Filtering, sorting, searching
- Error handling in shell
- Common jq filters
- Cron / scheduled patterns
- Token storage best practice
- Troubleshooting
| Requirement | Notes |
|---|---|
bash |
Any modern Linux/macOS — the scripts use set -euo pipefail |
curl |
7.68+ recommended (for --fail-with-body) |
jq |
1.6+ — required for parsing JSON responses |
| Network | Outbound to ACSMON_BASE_URL |
Windows users: run under WSL or Git Bash. Native cmd/PowerShell isn't covered here — use the Node.js or Python clients instead.
cd api-clients/bash-curl
chmod +x *.sh
export ACSMON_BASE_URL=https://monitoring.example.com
export [email protected]
export ACSMON_PASSWORD='your-strong-password'
# Get a token and capture it for the rest of the session
export ACSMON_TOKEN="$(./login.sh)"
./list-devices.sh
./poll-device.sh 42
./acknowledge-alert.sh 1234 "Investigating in INC-9421"Every script reads its config from these env vars:
| Variable | Required for | Purpose |
|---|---|---|
ACSMON_BASE_URL |
all | Root URL of your ACS Monitor install (no /api/v1 suffix) |
ACSMON_EMAIL |
login.sh only |
Email of an API-capable user |
ACSMON_PASSWORD |
login.sh only |
Password of that user |
ACSMON_TOKEN |
every other script | Bearer token (output of login.sh) |
Put these in your shell profile, in direnv's .envrc, or in a
private ~/.config/acsmon/env you source before invoking the
scripts. Never commit them.
| Script | What it does |
|---|---|
login.sh |
Exchange ACSMON_EMAIL + ACSMON_PASSWORD for a token (echoed to stdout). |
list-devices.sh |
Print every device in a tidy table, walking pagination automatically. |
poll-device.sh |
Trigger a one-off SNMP poll for a device by ID. |
acknowledge-alert.sh |
Acknowledge an open alert event with an optional note. |
Every script:
- Uses
set -euo pipefailso a failedcurlaborts the script. - Validates required env vars with
: "${VAR:?message}". - Calls
curl -fsSso HTTP 4xx/5xx errors propagate out. - Pipes JSON through
jqfor human-readable output.
TOKEN=$(curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/auth/login" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg e "$ACSMON_EMAIL" --arg p "$ACSMON_PASSWORD" \
'{email:$e, password:$p}')" \
| jq -er .token)
export ACSMON_TOKEN="$TOKEN"jq -er exits non-zero if .token is missing, so a bad-credentials
response will abort the script.
For cron jobs and CI, prefer a long-lived token issued to a dedicated
API user (Settings → Users in the UI). Just export ACSMON_TOKEN=...
and skip login.sh entirely.
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/auth/logout" \
-H "Authorization: Bearer $ACSMON_TOKEN"
unset ACSMON_TOKENcurl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=10" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Accept: application/json' | jq .curl -fsS "$ACSMON_BASE_URL/api/v1/devices?filter[status]=down&per_page=100" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Accept: application/json' \
| jq -r '.data[] | "\(.id)\t\(.name)\t\(.ip_address)"'DEVICE_ID=42
curl -fsS "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID" \
-H "Authorization: Bearer $ACSMON_TOKEN" | jq .DEVICE_ID=42
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID/poll" \
-H "Authorization: Bearer $ACSMON_TOKEN" | jq .DEVICE_ID=42
curl -fsS "$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID/metrics" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq '.data[] | {oid: .oid_name, value, unit, polled_at}'curl -fsS "$ACSMON_BASE_URL/api/v1/monitors?per_page=100" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq -r '.data[] | "\(.status)\t\(.type)\t\(.name)\t\(.host):\(.port // "-")"'curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/monitors" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "Public website",
"type": "https",
"host": "example.com",
"port": 443,
"check_interval_seconds": 60,
"timeout_ms": 5000,
"config": {
"method": "GET",
"path": "/",
"expected_status": 200,
"ssl_verify": true,
"alert_ssl_expiry_days": 14
}
}' | jq .MONITOR_ID=117
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/check" \
-H "Authorization: Bearer $ACSMON_TOKEN" | jq .MONITOR_ID=117
curl -fsS "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/results?per_page=20" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq -r '.data[] | "\(.checked_at)\t\(.status)\t\(.response_time_ms)ms"'MONITOR_ID=117
curl -fsS "$ACSMON_BASE_URL/api/v1/monitors/$MONITOR_ID/uptime?window=30d" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq '{uptime_percent, total_checks, failed_checks}'curl -fsS "$ACSMON_BASE_URL/api/v1/monitors?filter[type]=https&per_page=100" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq -r '.data[]
| select(.ssl_expiry_at != null)
| select(((.ssl_expiry_at|fromdateiso8601) - now) < 30*86400)
| "\(.name)\t\(.host)\t\(.ssl_expiry_at)"'curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=critical&sort=-triggered_at&per_page=50" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq '.data[] | {id, severity, triggered_at, target: (.monitor.name // .device.name)}'EVENT_ID=9821
NOTE='Investigating in INC-9421'
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$EVENT_ID/acknowledge" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg n "$NOTE" '{note: $n}')" | jq .EVENT_ID=9821
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$EVENT_ID/resolve" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"note":"Closed by automation"}' | jq .cutoff=$(date -u -d '24 hours ago' +%s)
curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=warning&per_page=100" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq -r --argjson cutoff "$cutoff" \
'.data[] | select((.triggered_at|fromdateiso8601) < $cutoff) | .id' \
| while read -r id; do
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/alert-events/$id/acknowledge" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"note":"Auto-ack >24h old"}' >/dev/null
echo "Acked $id"
donecurl -fsS "$ACSMON_BASE_URL/api/v1/system/health" \
-H "Authorization: Bearer $ACSMON_TOKEN" | jq .status=$(curl -fsS "$ACSMON_BASE_URL/api/v1/system/health" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq -r '.status')
[ "$status" = "ok" ] || { echo "ACS Monitor unhealthy: $status" >&2; exit 1; }Every list endpoint returns:
The simplest "walk every page" loop:
page=1
while :; do
body=$(curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=100&page=$page" \
-H "Authorization: Bearer $ACSMON_TOKEN")
echo "$body" | jq -r '.data[] | "\(.id)\t\(.name)"'
next=$(echo "$body" | jq -r '.next_page_url // empty')
[ -n "$next" ] || break
page=$((page + 1))
doneFor a quick total without iterating, use meta.total:
curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=1" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq '.meta.total'| Query param | Example | Effect |
|---|---|---|
search |
?search=core |
Free-text search across name/host fields |
filter[<field>] |
?filter[status]=down |
Exact-match filter (chainable with &) |
sort |
?sort=-last_polled_at |
Sort column. Prefix - for descending. |
per_page |
?per_page=100 |
Page size (cap depends on endpoint) |
page |
?page=3 |
Start page |
Combine freely (remember to URL-encode [ as %5B and ] as %5D
if your shell mangles them; curl accepts them literally in most
shells):
curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[severity]=critical&filter[status]=open&sort=-triggered_at&per_page=50" \
-H "Authorization: Bearer $ACSMON_TOKEN" | jq .Use set -euo pipefail and curl -fsS (or --fail-with-body on
curl 7.76+) so HTTP errors fail the script:
set -euo pipefail
# Fail loudly, show the error body
if ! body=$(curl --fail-with-body -sS "$ACSMON_BASE_URL/api/v1/devices/99999" \
-H "Authorization: Bearer $ACSMON_TOKEN" 2>&1); then
echo "Lookup failed: $body" >&2
exit 1
fi
echo "$body" | jq .To inspect status codes manually (e.g. to differentiate 404 from 500):
status=$(curl -sS -o /tmp/resp.json -w '%{http_code}' \
"$ACSMON_BASE_URL/api/v1/devices/$DEVICE_ID" \
-H "Authorization: Bearer $ACSMON_TOKEN")
case "$status" in
200) jq . /tmp/resp.json ;;
404) echo "Device $DEVICE_ID not found" >&2; exit 0 ;;
401) echo "Token expired — re-run login.sh" >&2; exit 2 ;;
*) echo "HTTP $status:" >&2; cat /tmp/resp.json >&2; exit 1 ;;
esac| Status | Meaning |
|---|---|
401 |
Token missing/expired — re-run login.sh |
403 |
Authenticated but lacks the permission slug for that endpoint |
404 |
Resource doesn't exist |
422 |
Validation error — body has { errors: { field: ["..."] } } |
429 |
Rate limited — sleep before retrying (Retry-After header) |
5xx |
Server bug or upstream outage — retry with backoff |
# Just the IDs
jq -r '.data[].id'
# CSV: name, status, last polled
jq -r '.data[] | [.name, .status, .last_polled_at] | @csv'
# Count by status
jq -r '.data | group_by(.status) | map({status: .[0].status, count: length})'
# Max response time across monitors
jq '[.data[].response_time_ms] | max'
# Pull a value out of nested config JSON
jq -r '.data[] | select(.type=="https") | .config.expected_status'
# Pretty-print just the first row
jq '.data[0]'
# Walk the next_page_url field if present
jq -r '.next_page_url // empty'# /etc/cron.d/acsmon-alert-count
0 * * * * acsmon /usr/local/bin/acsmon-alert-count.sh >>/var/log/acsmon.log 2>&1#!/usr/bin/env bash
# /usr/local/bin/acsmon-alert-count.sh
set -euo pipefail
source /etc/acsmon.env # exports ACSMON_BASE_URL + ACSMON_TOKEN
count=$(curl -fsS "$ACSMON_BASE_URL/api/v1/alert-events?filter[status]=open&filter[severity]=critical&per_page=1" \
-H "Authorization: Bearer $ACSMON_TOKEN" \
| jq '.meta.total')
curl -fsS -X POST https://metrics.example.com/ingest \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg c "$count" '{metric:"acsmon.alerts.open_critical", value:($c|tonumber)}')"# pulls a fresh token on Sunday and writes it to /etc/acsmon.env
if [ "$(date +%u)" = "7" ]; then
NEW=$(./login.sh)
sed -i "s|^export ACSMON_TOKEN=.*|export ACSMON_TOKEN=$NEW|" /etc/acsmon.env
ficutoff=$(date -u -d '1 hour ago' +%s)
page=1
while :; do
body=$(curl -fsS "$ACSMON_BASE_URL/api/v1/devices?per_page=100&page=$page" \
-H "Authorization: Bearer $ACSMON_TOKEN")
echo "$body" | jq -r --argjson cutoff "$cutoff" \
'.data[] | select((.last_polled_at // "1970-01-01T00:00:00Z" | fromdateiso8601) < $cutoff) | .id' \
| while read -r id; do
curl -fsS -X POST "$ACSMON_BASE_URL/api/v1/devices/$id/poll" \
-H "Authorization: Bearer $ACSMON_TOKEN" >/dev/null
echo "Re-polled $id"
done
next=$(echo "$body" | jq -r '.next_page_url // empty')
[ -n "$next" ] || break
page=$((page + 1))
done- Servers / cron: keep tokens in
/etc/acsmon.env(mode0600, owned by the service user) andsourceit from each script. - CI: inject via the platform's secret store (GitHub Actions secrets, GitLab CI variables, Drone, Jenkins credentials).
- Personal shell: put
export ACSMON_TOKEN=...in~/.acsmonrcwithchmod 600andsource ~/.acsmonrcfrom your.bashrc. - Never put tokens in repo files, terminal multiplexer scrollback
shared with others, or
~/.bash_history(useHISTCONTROL=ignorespaceand prefix sensitive commands with a space).
curl: (22) The requested URL returned error: 401 — Token
missing/expired. Regenerate with ./login.sh or re-issue the
long-lived token in the UI.
curl: (22) ... 403 — The user/role lacks the permission slug
for that endpoint. Grant the role via Settings → Users.
curl: (60) SSL certificate problem — Self-signed or untrusted
cert on the server. Either install a real cert, add the CA to the
system trust store, or pass -k (lab only — never in production).
jq: error: Cannot iterate over null (null) — The response had
no data field, often because of an HTTP error. Add -fsS (or
--fail-with-body) to your curl to surface the failure.
jq -er .token exits non-zero — login.sh got a 200 but no
token field. Almost always a wrong email/password or a deactivated
user.
Output is gibberish JSON — You forgot to pipe through jq (or
the response is genuinely binary, which the API never returns —
re-check the URL).
Pagination loop never ends — You're following next_page_url
without re-issuing it; copy the snippet under
Pagination patterns verbatim.
Bracket characters in filter URLs are eaten — Some shells parse
?filter[status]=down. Quote the whole URL: curl "...?filter[status]=down".
© Anglia Computer Solutions Ltd. — ACS Monitor is a product of Anglia Computer Solutions. Visit acsmon.com for documentation, demos and licensing.
{ "data": [ /* rows */ ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "from": 1, "to": 50, "per_page": 50, "total": 593 }, "next_page_url": "https://.../api/v1/devices?page=2" }