Developers
API reference
Authenticate with a personal API token and use the REST API (v1) to read servers, metrics, domains and incidents, and to act on incidents.
6 min read
On this page
ServerScope has a REST API (version 1) for reading monitoring data and working on incidents from your own tools: dashboards, chat bots, runbooks or automation such as n8n.
Note
The response examples on this page show the shape of each response. The values are examples, not data from a real environment.
Quick start
- Create a token under Profiel → API-tokens (see Creating a token).
- Store it in an environment variable, for example
SERVERSCOPE_API_TOKEN. Never put it in source code. - Request the active incidents:
curl -s "https://serverscope.nl/api/v1/incidents" \
-H "Authorization: Bearer $SERVERSCOPE_API_TOKEN" \
-H "Accept: application/json"
<?php
// Laravel's HTTP client; any HTTP client works the same way.
use Illuminate\Support\Facades\Http;
$incidents = Http::withToken(getenv('SERVERSCOPE_API_TOKEN'))
->acceptJson()
->get('https://serverscope.nl/api/v1/incidents')
->throw()
->json('data');
const response = await fetch('https://serverscope.nl/api/v1/incidents', {
headers: {
Authorization: `Bearer ${process.env.SERVERSCOPE_API_TOKEN}`,
Accept: 'application/json',
},
});
if (!response.ok) throw new Error(`ServerScope API: ${response.status}`);
const { data: incidents } = await response.json();
import os, requests
response = requests.get(
"https://serverscope.nl/api/v1/incidents",
headers={"Authorization": f"Bearer {os.environ['SERVERSCOPE_API_TOKEN']}", "Accept": "application/json"},
timeout=10,
)
response.raise_for_status()
incidents = response.json()["data"]
Creating a token
- Open Profiel → API-tokens (Profile → API tokens).
- Give the token a Naam (name) that says where it is used, for example
grafanaorn8n. - Choose the Bevoegdheden (abilities).
readis always included; only addwriteif the token must change incidents. - Choose the Geldigheid (validity): 30 days, 90 days, 1 year or unlimited.
- Click Token aanmaken (create token) and copy the token. It starts with
ssk_.
Warning
The token is shown once; ServerScope only stores a SHA-256 hash of it. A token acts with the permissions of your account, limited to its abilities. Revoke a token immediately if it may have leaked, and never paste it into a browser-based tool you do not control.
Authentication
Send the token as a bearer token on every request and ask for JSON:
GET /api/v1/servers HTTP/1.1
Host: serverscope.nl
Authorization: Bearer ssk_…
Accept: application/json
| Ability | Allows |
|---|---|
read |
All GET endpoints |
write |
The POST endpoints for incidents (acknowledge, investigate, resolve, comment) |
The monitoring agent uses its own endpoint (POST /api/agent/metrics) with per-server agent tokens. Agent tokens do not work for this API, and API tokens do not work for the agent.
Conventions
| Topic | Rule |
|---|---|
| Base URL | https://serverscope.nl/api/v1 |
| Format | JSON only; send Accept: application/json |
| Timestamps | ISO 8601, for example 2026-10-26T10:00:00+00:00 |
| Pagination | ?page=2&per_page=50 (1–100, default 25). Lists return data, links and meta. |
| Rate limit | 120 requests per minute per token by default (the operator can change it); above that 429 |
| Language | ServerScope's own error messages (401, 403, 409, 429) are in Dutch; validation messages follow the application's language setting |
A paginated list looks like this:
{
"data": [ … ],
"links": { "first": "…?page=1", "last": "…?page=3", "prev": null, "next": "…?page=2" },
"meta": { "current_page": 1, "last_page": 3, "per_page": 25, "total": 61, "…": "…" }
}
Endpoints
| Method | Path | Ability | Description |
|---|---|---|---|
GET |
/servers |
read | List servers |
GET |
/servers/{id} |
read | One server with its latest measurement |
GET |
/servers/{id}/metrics |
read | Measurements in a time window |
GET |
/domains |
read | List domains |
GET |
/domains/{id} |
read | One domain with HTTP, SSL, DNS and registration state |
GET |
/domains/{id}/checks |
read | Check history of a domain |
GET |
/incidents |
read | List incidents |
GET |
/incidents/{id} |
read | One incident with its timeline |
POST |
/incidents/{id}/acknowledge |
write | Acknowledge |
POST |
/incidents/{id}/investigate |
write | Start investigating |
POST |
/incidents/{id}/resolve |
write | Resolve |
POST |
/incidents/{id}/comments |
write | Add a note |
Servers
List servers
GET /servers
| Parameter | Type | Description |
|---|---|---|
status |
string | online, offline, pending or maintenance |
environment |
string | production, staging, development, testing or other |
per_page |
integer | 1–100, default 25 |
Results are sorted by name. Each server looks like this:
{
"id": 3,
"name": "web-01",
"hostname": "web-01.example.com",
"ip_address": "203.0.113.10",
"environment": "production",
"operating_system": "Ubuntu 24.04",
"agent_version": "1.0.0",
"status": "online",
"last_seen_at": "2026-10-26T10:00:00+00:00",
"maintenance": { "active": false, "starts_at": null, "ends_at": null },
"latest_metrics": {
"collected_at": "2026-10-26T10:00:00+00:00",
"cpu_percent": 23.4,
"memory_percent": 61.2,
"load_1": 0.42,
"uptime_seconds": 1762310
},
"created_at": "2026-09-24T08:12:00+00:00"
}
latest_metrics is null when no measurement has arrived yet.
Get one server
GET /servers/{id} returns {"data": { … }} with the same fields.
Measurements
GET /servers/{id}/metrics
| Parameter | Type | Description |
|---|---|---|
from |
date | Start of the window; default one hour ago |
to |
date | End of the window, after from; default now |
limit |
integer | 1–1440, default 120. The newest measurements in the window are returned, oldest first. |
This endpoint is not paginated:
{
"data": [
{
"collected_at": "2026-10-26T09:59:00+00:00",
"cpu_percent": 21.7,
"memory_used_bytes": 2621440000,
"memory_total_bytes": 4294967296,
"load_1": 0.38,
"load_5": 0.41,
"load_15": 0.35,
"disks": [ … ],
"network_rx_bytes": 918273645,
"network_tx_bytes": 129384756
}
]
}
Domains
List domains
GET /domains
| Parameter | Type | Description |
|---|---|---|
status |
string | healthy, warning, critical or unknown (active domains only) |
per_page |
integer | 1–100, default 25 |
{
"id": 7,
"name": "example.com",
"display_name": "example.com",
"url": "https://example.com/",
"status": "healthy",
"monitoring_active": true,
"http": { "enabled": true, "status": "up", "last_status_code": 200, "last_response_ms": 182, "consecutive_failures": 0, "last_checked_at": "2026-10-26T10:00:00+00:00" },
"ssl": { "enabled": true, "status": "valid", "issuer": "…", "valid_to": "2026-12-06T23:59:59+00:00", "days_remaining": 41 },
"dns": { "enabled": true, "status": "ok", "last_changed_at": null },
"registration": { "enabled": true, "status": "verified", "expires_at": "2027-03-01T00:00:00+00:00", "registrar": "…", "source": "rdap", "checked_at": "2026-10-26T03:00:00+00:00" },
"created_at": "2026-09-24T08:30:00+00:00"
}
registration.status is verified only when the registry publishes an expiry date.
Check history
GET /domains/{id}/checks
| Parameter | Type | Description |
|---|---|---|
type |
string | http, ssl, dns or expiry |
per_page |
integer | 1–100, default 50 |
Newest first, with its own meta (current_page, last_page, per_page, total) and links (next, prev):
{
"data": [
{ "type": "http", "result": "…", "success": true, "response_time_ms": 182, "http_status_code": 200, "error_code": null, "message": null, "checked_at": "2026-10-26T10:00:00+00:00" }
],
"meta": { "current_page": 1, "last_page": 12, "per_page": 50, "total": 583 },
"links": { "next": "…?page=2", "prev": null }
}
Incidents
List incidents
GET /incidents
| Parameter | Type | Description |
|---|---|---|
state |
string | active (default: open, acknowledged or investigating), resolved, closed or all |
severity |
string | info, warning or critical |
per_page |
integer | 1–100, default 25 |
Newest first:
{
"id": 12,
"title": "shop.example.com is onbereikbaar",
"description": "…",
"severity": "critical",
"status": "acknowledged",
"source": "http",
"subject": { "type": "domain", "id": 7, "name": "shop.example.com" },
"occurrence_count": 4,
"reopened_count": 0,
"assigned_to": 2,
"opened_at": "2026-10-26T10:02:00+00:00",
"last_occurrence_at": "2026-10-26T10:08:00+00:00",
"acknowledged_at": "2026-10-26T10:04:00+00:00",
"resolved_at": null,
"closed_at": null,
"resolution_notes": null
}
source is one of alert, agent, http, ssl, ssl_expiry, dns, expiry or manual.
Get one incident
GET /incidents/{id} adds events, the timeline:
"events": [
{ "type": "opened", "message": "…", "user_id": null, "created_at": "2026-10-26T10:02:00+00:00" },
{ "type": "acknowledged", "message": "…", "user_id": 2, "created_at": "2026-10-26T10:04:00+00:00" }
]
Acknowledge, investigate or resolve
POST /incidents/{id}/acknowledge, POST /incidents/{id}/investigate, POST /incidents/{id}/resolve
| Parameter | Type | Description |
|---|---|---|
notes |
string | Optional, at most 5000 characters (for resolve: the resolution) |
Returns the updated incident including events. Only these transitions are allowed:
| From | To |
|---|---|
open |
acknowledged, investigating, resolved |
acknowledged |
investigating, resolved |
investigating |
acknowledged, resolved |
Any other transition returns 409 Conflict.
curl -s -X POST "https://serverscope.nl/api/v1/incidents/12/resolve" \
-H "Authorization: Bearer $SERVERSCOPE_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"notes": "Disk cleaned up and log rotation fixed."}'
$incident = Http::withToken(getenv('SERVERSCOPE_API_TOKEN'))
->acceptJson()
->post('https://serverscope.nl/api/v1/incidents/12/resolve', ['notes' => 'Disk cleaned up and log rotation fixed.'])
->throw()
->json('data');
const response = await fetch('https://serverscope.nl/api/v1/incidents/12/resolve', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SERVERSCOPE_API_TOKEN}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ notes: 'Disk cleaned up and log rotation fixed.' }),
});
response = requests.post(
"https://serverscope.nl/api/v1/incidents/12/resolve",
headers={"Authorization": f"Bearer {os.environ['SERVERSCOPE_API_TOKEN']}", "Accept": "application/json"},
json={"notes": "Disk cleaned up and log rotation fixed."},
timeout=10,
)
Add a note
POST /incidents/{id}/comments
| Parameter | Type | Description |
|---|---|---|
message |
string | Required, at most 5000 characters |
Returns 201 Created:
{ "data": { "type": "comment", "message": "Restarted php-fpm.", "created_at": "2026-10-26T10:11:00+00:00" } }
Errors
Errors are JSON with a message and, for validation errors (422), an errors object with the messages per field:
{
"message": "…",
"errors": { "per_page": ["…"] }
}
Use the HTTP status code, not the message text, to decide what to do.
| Status | Meaning | What to do |
|---|---|---|
401 |
Token missing, malformed, expired or revoked (Ontbrekend of ongeldig API-token.) | Check the Authorization header; create a new token if needed |
403 |
The token lacks the ability, for example write (Dit token mist de bevoegdheid 'write'.) |
Create a token with the right abilities |
404 |
The object does not exist | Check the id |
409 |
Incident transition not allowed | Read the incident first and check its status |
422 |
Validation failed | See errors |
429 |
Too many requests (Te veel verzoeken.) | Wait and retry with a back-off |
Good practice
- Use one token per integration, with only the abilities it needs and an expiry date.
- Keep tokens in a secret store or environment variable, never in a repository or front-end code.
- Back off and retry on
429and5xx; do not retry4xxerrors other than429without changing the request. - Want ServerScope to call you instead of polling? Use webhooks.
Try it in the application
Log in to ServerScope and follow the steps in your own environment.
Log inSomething unclear or missing?
Tell us which part of this page did not help; never include tokens or passwords.
Send feedback