Open-source PHP client library for the ACS Monitor REST API — the network and infrastructure monitoring platform from Anglia Computer Solutions.
PSR-4 client class for PHP 8.1+. Drop the src/ folder into another
project's composer.json (or use this folder as a standalone composer
package) and you've got typed access to the API. Composer pulls in a
single HTTP-client dependency on first install.
Licensing: this PHP client is open source under the MIT licence. The ACS Monitor application it talks to is licensed commercial software with a free tier for installations up to 100 monitored resources — see acsmon.com for tiers and pricing.
- Requirements
- Installation
- Quick start
- Configuration
- Authentication
- Method reference
- Pagination
- Filtering, sorting, searching
- Working with monitors
- Working with alerts
- Error handling
- Custom HTTP options (timeouts, proxy, TLS)
- Token storage best practice
- Recipes
- Troubleshooting
| Requirement | Version |
|---|---|
| PHP | 8.1 or newer (uses readonly + first-class enums) |
| Composer | 2.x |
| HTTP client | pulled in automatically as a dependency |
| Network | outbound HTTPS to your ACS Monitor instance |
The client itself has no other dependencies and works standalone in plain CLI scripts, web frameworks, queue workers, or libraries.
cd api-clients/php
composer installCopy src/AcsMonitorClient.php into your project (under
AcsMon\AcsMonitorClient) and add the HTTP client to your own
composer.json:
composer require guzzlehttp/guzzle:^7.0Then add to your autoload.psr-4:
Run composer dump-autoload and you're done.
export ACSMON_BASE_URL=https://monitoring.example.com
export [email protected]
export ACSMON_PASSWORD='your-strong-password'
php examples/list-devices.php
php examples/poll-device.php 42If you'd rather use a long-lived token (recommended for production
integrations) skip the email/password vars and set ACSMON_TOKEN
instead. The client will use it directly.
Constructor signature:
public function __construct(
string $baseUrl,
?string $token = null,
array $guzzleOptions = []
)| Param | Type | Description |
|---|---|---|
$baseUrl |
string | e.g. https://monitoring.example.com (no trailing slash needed) |
$token |
?string | Optional bearer token; if null, you must call login() before any other method |
$guzzleOptions |
array | Merged into the underlying HTTP client options (timeout, proxy, TLS verify, custom CA bundle, etc.) |
The constructor does not perform any network I/O — instantiating the client is safe in any context including service-container bindings.
$api = new AcsMonitorClient('https://monitoring.example.com');
$token = $api->login('[email protected]', 's3cret');
// $token is also stored on the client instanceThe token persists until either of these happens:
- you explicitly call
$api->logout()(revokes the token server-side) - the user account is deleted
Re-using the same token across processes is fine — store it in your secret manager and skip the login step.
$api = new AcsMonitorClient('https://monitoring.example.com', '1|abc...');
// no login() call needed$api->token(); // returns ?string
$api->setToken($t); // replace the in-memory token
$api->setToken(null); // forget the token (useful before re-login)Every method below maps 1:1 to an API endpoint. Names follow the endpoint structure where reasonable.
| Signature | HTTP |
|---|---|
login(string $email, string $password): string |
POST /auth/login — returns the bearer token |
logout(): void |
POST /auth/logout — revokes the token |
token(): ?string |
local accessor |
setToken(?string $token): void |
local mutator |
| Signature | HTTP |
|---|---|
devices(array $query = []): iterable |
GET /devices — yields each row, paginated transparently |
device(int $id): array |
GET /devices/{id} |
pollDevice(int $id): array |
POST /devices/{id}/poll |
deviceMetrics(int $id, array $query = []): array |
GET /devices/{id}/metrics |
Common $query keys:
per_page, page, search, sort, filter[status], filter[group],
filter[snmp_version], filter[is_active].
| Signature | HTTP |
|---|---|
monitors(array $query = []): iterable |
GET /monitors — yields each row |
monitor(int $id): array |
GET /monitors/{id} |
createMonitor(array $payload): array |
POST /monitors |
checkMonitor(int $id): array |
POST /monitors/{id}/check |
monitorResults(int $id, array $query = []): array |
GET /monitors/{id}/results |
monitorUptime(int $id, array $query = []): array |
GET /monitors/{id}/uptime |
| Signature | HTTP |
|---|---|
alertEvents(array $query = []): iterable |
GET /alert-events |
acknowledgeAlertEvent(int $id, ?string $note = null): array |
POST /alert-events/{id}/acknowledge |
resolveAlertEvent(int $id, ?string $note = null): array |
POST /alert-events/{id}/resolve |
| Signature | HTTP |
|---|---|
systemHealth(): array |
GET /system/health |
All single-resource methods return a array<string,mixed> decoded
straight from the JSON response. Iterator methods yield one such
array per row.
Example device row:
[
'id' => 42,
'name' => 'core-sw1',
'ip_address' => '10.0.0.4',
'snmp_version' => '2c',
'community_string' => 'public',
'port' => 161,
'status' => 'up',
'snmp_enabled' => true,
'is_active' => true,
'last_polled_at' => '2026-04-26T14:32:01+00:00',
'group' => ['id' => 3, 'name' => 'Core Network'],
// ...
]Example monitor row:
[
'id' => 17,
'name' => 'Public website',
'type' => 'https',
'host' => 'example.com',
'port' => 443,
'status' => 'up',
'response_time_ms' => 412,
'check_interval_seconds' => 60,
'last_checked_at' => '2026-04-26T14:31:57+00:00',
'uptime_percent_7d' => 99.97,
'config' => ['method' => 'GET', 'path' => '/', 'expected_status' => 200],
]Iterator methods (devices(), monitors(), alertEvents()) yield one
row at a time and follow next_page_url automatically. You don't need
to track the page counter yourself:
foreach ($api->devices() as $device) {
// every device, across all pages
}Want to limit how much you pull? Use the standard per_page parameter
(server caps at 100 per request) and break out of the loop:
$collected = [];
foreach ($api->devices(['per_page' => 50, 'sort' => '-last_polled_at']) as $device) {
$collected[] = $device;
if (count($collected) >= 100) break; // first 100 most-recently-polled
}Every list endpoint accepts the same query syntax:
| Param | Example | Purpose |
|---|---|---|
search |
search=router |
substring match across name/IP |
filter[field] |
filter[status]=down |
exact match |
filter[field] |
filter[snmp_version]=3 |
exact match |
sort |
sort=name / sort=-last_polled_at |
column name; - for descending |
per_page |
per_page=50 |
rows per page (max 100) |
page |
page=2 |
only set manually if you're NOT iterating |
Pass them as a normal PHP associative array:
$query = [
'filter[status]' => 'down',
'filter[snmp_version]' => '2c',
'sort' => '-last_polled_at',
'per_page' => 100,
];
foreach ($api->devices($query) as $device) {
// …
}$monitor = $api->createMonitor([
'name' => 'Public site (HTTPS)',
'type' => 'https',
'host' => 'example.com',
'port' => 443,
'check_interval_seconds' => 60,
'timeout_ms' => 10000,
'is_active' => true,
'config' => [
'method' => 'GET',
'path' => '/health',
'expected_status' => 200,
'follow_redirects' => true,
'ssl_verify' => true,
'allow_self_signed' => false,
'expected_body_regex' => '"status":"ok"',
],
]);$result = $api->checkMonitor($monitor['id']);
echo $result['status']; // 'up' | 'down' | 'degraded'
echo $result['response_time_ms']; // ms$results = $api->monitorResults($monitor['id'], ['per_page' => 100]);
foreach ($results['data'] ?? [] as $check) {
echo "{$check['checked_at']}: {$check['status']} ({$check['response_time_ms']}ms)\n";
}$uptime = $api->monitorUptime($monitor['id'], ['days' => 30]);
echo "{$uptime['uptime_percent']}% over {$uptime['period_days']}d\n";ping, tcp, http, https, ssl, ssh, smtp, ftp, pop3,
imap, dns, mysql, snmp. Each has its own config schema —
see the main repository's CLAUDE.md for the full spec.
$query = ['filter[status]' => 'open', 'filter[severity]' => 'critical'];
foreach ($api->alertEvents($query) as $event) {
printf("[%s] %s — %s\n",
$event['severity'],
$event['triggered_at'],
$event['device']['name'] ?? $event['monitor']['name'] ?? 'Unknown',
);
}$api->acknowledgeAlertEvent(123, 'Investigating in INC-9421');The optional note is recorded on the event timeline and visible in the UI.
$api->resolveAlertEvent(123, 'Patched and confirmed up');Anything non-2xx becomes a RuntimeException with the upstream
response body and HTTP status code embedded in the message and code.
try {
$api->pollDevice(99999);
} catch (\RuntimeException $e) {
error_log("Poll failed: HTTP {$e->getCode()} — {$e->getMessage()}");
}Status codes you should handle distinctly:
| Code | When | What to do |
|---|---|---|
| 401 | token expired/invalid | re-login, then retry |
| 403 | token lacks the required permission | escalate to whoever provisioned the API user |
| 404 | resource doesn't exist OR isn't visible to this user | inspect ID; check role |
| 422 | validation failed on a POST/PUT | parse errors from the response body |
| 429 | rate-limited | sleep & retry with exponential backoff |
| 5xx | server-side problem | retry with backoff; alert if persistent |
A defensive wrapper:
function callWithRetry(callable $fn, int $maxAttempts = 3): mixed
{
$attempt = 0;
do {
try {
return $fn();
} catch (\RuntimeException $e) {
$code = $e->getCode();
if ($code >= 500 || $code === 429) {
if (++$attempt >= $maxAttempts) throw $e;
sleep(2 ** $attempt);
continue;
}
throw $e;
}
} while (true);
}
$device = callWithRetry(fn () => $api->device(42));The third constructor argument is merged into the underlying HTTP client options, so anything the HTTP client supports works:
$api = new AcsMonitorClient('https://monitoring.example.com', null, [
'timeout' => 60,
'connect_timeout' => 5,
'verify' => '/etc/ssl/certs/ca-certificates.crt', // custom CA bundle
'proxy' => 'http://proxy.local:3128',
'headers' => [
'User-Agent' => 'MyIntegration/1.0',
],
]);For testing against a self-signed dev box, set 'verify' => false
but never in production.
Production integrations should:
- Provision a dedicated API user in ACS Monitor with the minimum
permission scope needed (often
viewerplusmonitors.run_toolsfor triggering polls/checks). - Generate a long-lived personal access token for that user.
- Store the token in your secret manager (HashiCorp Vault, AWS
Secrets Manager, Doppler,
.env.gpg, …) — never in code or git. - Pass it via env var to your application so the same client code works in dev / staging / prod with zero code changes.
- Rotate periodically by issuing a new token, deploying it, then
revoking the old one with
$api->logout()(or via the UI).
foreach ($api->devices() as $device) {
Cmdb::upsertAsset([
'external_id' => "acsmon-{$device['id']}",
'name' => $device['name'],
'ip_address' => $device['ip_address'],
'tags' => ['acsmon', $device['group']['name'] ?? 'ungrouped'],
'last_seen_at' => $device['last_polled_at'],
]);
}$query = [
'filter[status]' => 'open',
'filter[severity]' => 'critical',
'sort' => '-triggered_at',
];
foreach ($api->alertEvents($query) as $event) {
PagerDuty::createIncident([
'dedup_key' => "acsmon-{$event['id']}",
'summary' => $event['title'] ?? $event['message'],
'source' => $event['device']['name'] ?? $event['monitor']['name'],
'severity' => 'critical',
]);
$api->acknowledgeAlertEvent($event['id'], 'Forwarded to PagerDuty');
}// After deploying app, immediately check that its monitor still passes
$result = $api->checkMonitor(getenv('ACSMON_DEPLOY_MONITOR_ID'));
if ($result['status'] !== 'up') {
fwrite(STDERR, "Post-deploy check failed: {$result['error']}\n");
exit(1);
}
echo "Health check passed in {$result['response_time_ms']}ms\n";$report = [];
foreach ($api->monitors(['filter[is_active]' => true]) as $monitor) {
$u = $api->monitorUptime($monitor['id'], ['days' => 7]);
$report[] = [
'name' => $monitor['name'],
'host' => $monitor['host'],
'uptime_pct_7d' => $u['uptime_percent'] ?? null,
];
}
usort($report, fn ($a, $b) => $a['uptime_pct_7d'] <=> $b['uptime_pct_7d']);
// … render to email / Slack / whereverLogin returns 422 "The given data was invalid"
Most often email or password is missing/wrong. The error response
body contains the per-field complaint.
Calls return 401 even with a token set The token has been revoked or the user account was disabled. Re-login.
Calls return 403 Authentication is fine but the user lacks the necessary permission. Either grant the role/permission in the UI or have the integration act as a user that already has it.
Iterator methods yield nothing
Likely a too-restrictive filter[] query, or you're targeting a user
that doesn't have visibility on any devices/monitors. Try without
filters first.
Long-running scripts get cut off mid-fetch
Bump 'timeout' in the constructor's options array. The default 30s
covers normal use; uptime queries spanning months can take longer.
Self-signed cert errors on the dev box
Pass 'verify' => false in the constructor options (dev only) or
provide a custom CA bundle path.
© Anglia Computer Solutions Ltd. — ACS Monitor is a product of Anglia Computer Solutions. Visit acsmon.com for documentation, demos and licensing.
{ "autoload": { "psr-4": { "AcsMon\\": "path/to/src/" } } }