API Reference
Complete REST API documentation for the Hermes FastAPI backend.
Base URL
http://localhost:8080/api
Replace localhost:8080 with your server address.
Authentication
API Key Authentication
When API_KEY is set in environment variables, protected endpoints require an X-Api-Key header:
curl -X POST http://localhost:8080/api/trigger \
-H "X-Api-Key: your-api-key-here"
Public endpoints (GET requests) do not require authentication.
Generate Secure API Key
# Option 1: Python secrets module (recommended)
python -c 'import secrets; print(secrets.token_urlsafe(32))'
# Option 2: OpenSSL
openssl rand -hex 32
Requirements:
- Minimum 32 characters (enforced at startup)
- Application exits with error if
API_KEYis set but too short
Disabling Authentication
Leave API_KEY unset in .env to disable authentication entirely. Not recommended for production.
Rate Limiting
Protected endpoints are rate-limited per API key:
- Default: 60 requests per 60-second sliding window
- Configurable: Set
RATE_LIMIT_PER_MINUTEin.env - Response on limit:
429 Too Many RequestswithRetry-Afterheader
Example rate limit response:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"detail": "Rate limit exceeded. Try again in 60 seconds."
}
Public Endpoints
Health Check
Get API health and scheduler status.
Request:
GET /api/health
Response:
{
"status": "healthy",
"scheduler_running": true,
"uptime_seconds": 3600,
"last_test_timestamp": "2026-04-29T12:00:00Z"
}
Status Codes:
200 OK— API healthy
Get Results (Paginated)
Retrieve speed test results with pagination. Reads from SQLite if available, falls back to CSV.
Request:
GET /api/results?page=1&page_size=50
Query Parameters:
page(optional, default:1) — Page number (1-indexed)page_size(optional, default:50) — Results per page (max: 500)
Response:
{
"results": [
{
"id": 123,
"timestamp": "2026-04-29T12:00:00Z",
"download_mbps": 250.5,
"upload_mbps": 35.2,
"ping_ms": 15.3,
"jitter_ms": 2.1,
"isp": "Comcast"
}
],
"pagination": {
"page": 1,
"page_size": 50,
"total": 500,
"total_pages": 10
},
"source": "sqlite"
}
Status Codes:
200 OK— Results returned404 Not Found— Page out of range or no results available
cURL Example:
curl http://localhost:8080/api/results?page=1&page_size=100
Get Latest Result
Retrieve the most recent speed test result.
Request:
GET /api/results/latest
Response:
{
"id": 123,
"timestamp": "2026-04-29T12:00:00Z",
"download_mbps": 250.5,
"upload_mbps": 35.2,
"ping_ms": 15.3,
"jitter_ms": 2.1,
"isp": "Comcast"
}
Status Codes:
200 OK— Result returned404 Not Found— No results available
cURL Example:
curl http://localhost:8080/api/results/latest
Get Configuration
Retrieve current runtime configuration (interval, enabled exporters).
Request:
GET /api/config
Response:
{
"speedtest_interval_minutes": 60,
"enabled_exporters": ["csv", "sqlite", "prometheus"]
}
Status Codes:
200 OK— Configuration returned
cURL Example:
curl http://localhost:8080/api/config
Get Alert Configuration
Retrieve current alert settings and provider configuration.
Request:
GET /api/alerts
Response:
{
"enabled": true,
"failure_threshold": 3,
"cooldown_minutes": 60,
"providers": {
"webhook": {
"enabled": false,
"url": ""
},
"gotify": {
"enabled": false,
"url": "",
"token": "",
"priority": 5
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "hermes_alerts",
"token": "",
"priority": 3,
"tags": "warning,rotating_light"
},
"apprise": {
"enabled": false,
"url": "",
"urls": []
}
}
}
Status Codes:
200 OK— Configuration returned
cURL Example:
curl http://localhost:8080/api/alerts
Get Outage Events
Retrieve paginated outage detection history. Requires SQLite to be enabled.
Request:
GET /api/outages?page=1&page_size=50
Query Parameters:
page(optional, default:1) — Page number (1-indexed)page_size(optional, default:50) — Events per page (max: 500)
Response:
{
"events": [
{
"id": 1,
"event_type": "outage_start",
"timestamp": "2026-04-29T03:12:00Z",
"duration_seconds": 142.5,
"isp_name": "Comcast",
"asn": "AS7922",
"bgp_unstable": false,
"cloudflare_outage_desc": null,
"probe_results": "{\"1.1.1.1:53\": false, \"8.8.8.8:53\": false, \"9.9.9.9:53\": true}"
}
],
"total": 1,
"page": 1,
"page_size": 50
}
Status Codes:
200 OK— Events returned503 Service Unavailable— Database not yet available
cURL Example:
curl http://localhost:8080/api/outages
Get Current Outage Status
Check whether an outage is currently in progress.
Request:
GET /api/outage-status
Response:
{
"outage_in_progress": false,
"outage_start_time": null
}
Status Codes:
200 OK— Status returned
cURL Example:
curl http://localhost:8080/api/outage-status
Get Anomaly Detection Results
Return recent speed test results annotated with z-score anomaly detection. Each result is compared against a rolling baseline window.
Request:
GET /api/analysis/anomalies?limit=50&window=20&threshold=2.5
Query Parameters:
limit(optional, default:50, max:500) — Number of results to annotatewindow(optional, default:20, range:3–200) — Baseline window size for z-score computationthreshold(optional, default:2.5, range:0.5–10.0) — Z-score magnitude that triggers a flag
Response:
[
{
"id": 123,
"timestamp": "2026-04-29T12:00:00Z",
"download_mbps": 12.3,
"upload_mbps": 35.2,
"ping_ms": 15.3,
"jitter_ms": 2.1,
"isp_name": "Comcast",
"server_name": "Chicago, IL",
"server_location": "Chicago, IL",
"is_anomaly": true,
"anomaly_flags": [
{
"metric": "download_mbps",
"value": 12.3,
"baseline_mean": 248.5,
"baseline_stdev": 15.2,
"z_score": -15.5
}
]
}
]
Status Codes:
200 OK— Results returned503 Service Unavailable— Database not yet available
cURL Example:
curl "http://localhost:8080/api/analysis/anomalies?limit=100&threshold=3.0"
Get Time-of-Day Analysis
Return average download, upload, and ping grouped by hour of day (UTC).
Request:
GET /api/analysis/time-of-day?days=30
Query Parameters:
days(optional, default:30, range:0–3650) — Lookback window in days. Use0for all time.
Response:
[
{
"hour": 2,
"sample_count": 28,
"avg_download_mbps": 310.5,
"avg_upload_mbps": 42.1,
"avg_ping_ms": 12.3,
"min_download_mbps": 290.0,
"max_download_mbps": 340.2
}
]
Status Codes:
200 OK— Stats returned (empty array if no data in window)
cURL Example:
curl "http://localhost:8080/api/analysis/time-of-day?days=90"
Get Trend Analysis
Return month-over-month statistics and linear regression slopes to detect long-term degradation.
Request:
GET /api/analysis/trends?months=6
Query Parameters:
months(optional, default:6, range:0–120) — Lookback window in calendar months. Use0for all time.
Response:
{
"monthly_stats": [
{
"month": "2026-04",
"sample_count": 720,
"avg_download_mbps": 248.5,
"avg_upload_mbps": 35.2,
"avg_ping_ms": 15.3
}
],
"download_slope": -2.1,
"upload_slope": -0.5,
"ping_slope": 0.8,
"degradation_detected": true,
"months_available": 6
}
Slopes are expressed as Mbps (or ms) per calendar month. Negative download/upload slope or positive ping slope indicates degradation.
Status Codes:
200 OK— Report returned
cURL Example:
curl "http://localhost:8080/api/analysis/trends?months=12"
Check Trigger Status
Check if a speed test is currently running.
Request:
GET /api/trigger/status
Response:
{
"is_running": false
}
Status Codes:
200 OK— Status returned
cURL Example:
curl http://localhost:8080/api/trigger/status
Protected Endpoints
Require X-Api-Key header when API_KEY environment variable is set.
Trigger Speed Test
Manually trigger a speed test to run immediately.
Request:
POST /api/trigger
Headers:
X-Api-Key: your-api-key-here
Response:
{
"status": "started"
}
OR if a test is already running:
{
"status": "already_running"
}
Status Codes:
200 OK— Test triggered successfully or already running401 Unauthorized— Missing or invalid API key429 Too Many Requests— Rate limit exceeded
cURL Example:
curl -X POST http://localhost:8080/api/trigger \
-H "X-Api-Key: your-api-key-here"
Update Configuration
Update runtime configuration (interval, enabled exporters).
Request:
PUT /api/config
Content-Type: application/json
Headers:
X-Api-Key: your-api-key-here
Content-Type: application/json
Body:
{
"interval_minutes": 30,
"enabled_exporters": ["csv", "sqlite", "prometheus", "loki"],
"scanning_enabled": true
}
Response:
{
"interval_minutes": 30,
"enabled_exporters": ["csv", "sqlite"],
"scanning_enabled": true
}
Validation:
interval_minutesmust be between 5 and 1440 minutes (5 minutes to 24 hours)enabled_exportersmust be a list containing valid exporters:csv,sqlite,prometheus,loki,influxdbscanning_enabledcontrols whether automatic tests are running
Status Codes:
200 OK— Configuration updated400 Bad Request— Invalid input401 Unauthorized— Missing or invalid API key429 Too Many Requests— Rate limit exceeded
cURL Example:
curl -X PUT http://localhost:8080/api/config \
-H "X-Api-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"interval_minutes": 30,
"enabled_exporters": ["csv", "sqlite"],
"scanning_enabled": true
}'
Update Alert Configuration
Update alert settings and provider configuration.
Request:
PUT /api/alerts
Content-Type: application/json
Headers:
X-Api-Key: your-api-key-here
Content-Type: application/json
Body:
{
"enabled": true,
"failure_threshold": 3,
"cooldown_minutes": 60,
"providers": {
"webhook": {
"enabled": false,
"url": ""
},
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "hermes_alerts",
"token": "",
"priority": 3,
"tags": "warning,rotating_light"
}
}
}
Response:
{
"status": "success",
"message": "Alert configuration updated successfully"
}
Validation:
failure_thresholdmust be >= 0 (0 = disabled)cooldown_minutesmust be >= 1- Alert URLs validated for SSRF protection (see Security)
- Provider-specific validation (priority ranges, required fields)
Status Codes:
200 OK— Configuration updated400 Bad Request— Invalid input or SSRF risk detected401 Unauthorized— Missing or invalid API key429 Too Many Requests— Rate limit exceeded
cURL Example:
curl -X PUT http://localhost:8080/api/alerts \
-H "X-Api-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"failure_threshold": 3,
"cooldown_minutes": 60,
"providers": {
"ntfy": {
"enabled": true,
"url": "https://ntfy.sh",
"topic": "hermes_alerts",
"priority": 3
}
}
}'
Test Alert Notification
Send a test notification to all enabled alert providers.
Request:
POST /api/alerts/test
Headers:
X-Api-Key: your-api-key-here
Response:
{
"status": "success",
"message": "Test alert sent successfully to 2 provider(s)",
"results": {
"webhook": true,
"ntfy": true,
"gotify": false,
"apprise": false
}
}
Rate Limiting:
- Test alerts are rate-limited globally (10-second cooldown)
- Separate from per-API-key rate limits
- Prevents notification spam
Status Codes:
200 OK— Test sent (checkresultsfor per-provider status)401 Unauthorized— Missing or invalid API key429 Too Many Requests— Test alert cooldown active (wait 10 seconds)
cURL Example:
curl -X POST http://localhost:8080/api/alerts/test \
-H "X-Api-Key: your-api-key-here"
Error Responses
All errors return JSON with detail field:
400 Bad Request
{
"detail": "speedtest_interval_minutes must be between 1 and 1440"
}
401 Unauthorized
{
"detail": "Invalid API key"
}
404 Not Found
{
"detail": "No results found"
}
413 Payload Too Large
{
"detail": "Request body exceeds maximum size of 1048576 bytes"
}
429 Too Many Requests
{
"detail": "Rate limit exceeded. Try again in 60 seconds."
}
Response includes Retry-After header with seconds to wait.
503 Service Unavailable
{
"detail": "Speed test already running"
}
Security Features
SSRF Protection
Alert URLs are validated to prevent Server-Side Request Forgery attacks:
Blocked:
- Non-HTTP schemes:
file://,ftp://,data:// - Localhost:
localhost,127.0.0.1,::1 - Private IP ranges:
10.x,192.168.x,172.16-31.x - Link-local:
169.254.x,fe80::/10 - Cloud metadata endpoints:
169.254.169.254
Allowed:
- Public HTTPS/HTTP URLs only
See Security Guide for details.
Security Headers
All responses include security headers:
X-Frame-Options: DENYX-Content-Type-Options: nosniffCross-Origin-Resource-Policy: same-originReferrer-Policy: strict-origin-when-cross-origin
CORS Configuration
CORS is configured via CORS_ORIGINS environment variable:
CORS_ORIGINS=https://your-frontend-domain.com
Allowed methods: GET, POST, PUT
Allowed headers: Content-Type, X-Api-Key
API Client Examples
Python (requests)
import requests
BASE_URL = "http://localhost:8080/api"
API_KEY = "your-api-key-here"
# Get latest result
response = requests.get(f"{BASE_URL}/results/latest")
result = response.json()
print(f"Download: {result['download_mbps']} Mbps")
# Trigger test
response = requests.post(
f"{BASE_URL}/trigger",
headers={"X-Api-Key": API_KEY}
)
print(response.json())
# Update config
response = requests.put(
f"{BASE_URL}/config",
headers={
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
},
json={
"speedtest_interval_minutes": 30,
"enabled_exporters": ["csv", "sqlite"]
}
)
print(response.json())
JavaScript (fetch)
const BASE_URL = "http://localhost:8080/api";
const API_KEY = "your-api-key-here";
// Get latest result
const response = await fetch(`${BASE_URL}/results/latest`);
const result = await response.json();
console.log(`Download: ${result.download_mbps} Mbps`);
// Trigger test
const triggerResponse = await fetch(`${BASE_URL}/trigger`, {
method: "POST",
headers: {
"X-Api-Key": API_KEY
}
});
console.log(await triggerResponse.json());
// Update config
const configResponse = await fetch(`${BASE_URL}/config`, {
method: "PUT",
headers: {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
speedtest_interval_minutes: 30,
enabled_exporters: ["csv", "sqlite"]
})
});
console.log(await configResponse.json());
Bash (curl)
#!/bin/bash
BASE_URL="http://localhost:8080/api"
API_KEY="your-api-key-here"
# Get latest result
curl -s "$BASE_URL/results/latest" | jq .
# Trigger test
curl -X POST "$BASE_URL/trigger" \
-H "X-Api-Key: $API_KEY"
# Update config
curl -X PUT "$BASE_URL/config" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"speedtest_interval_minutes": 30,
"enabled_exporters": ["csv", "sqlite"]
}'
# Get paginated results
curl -s "$BASE_URL/results?page=1&page_size=100" | jq .
WebSocket Support
Hermes does not currently support WebSocket connections. For real-time updates:
Option 1: Poll /api/results/latest periodically
setInterval(async () => {
const response = await fetch("/api/results/latest");
const result = await response.json();
updateUI(result);
}, 30000); // Poll every 30 seconds
Option 2: Poll /api/trigger/status to detect test completion
async function waitForTestCompletion() {
while (true) {
const response = await fetch("/api/trigger/status");
const status = await response.json();
if (!status.running) break;
await new Promise(resolve => setTimeout(resolve, 5000));
}
// Fetch new result
const result = await fetch("/api/results/latest");
return result.json();
}
See Also
- Getting Started — Deployment and authentication setup
- Security Guide — API key best practices, SSRF protection
- Alert Configuration — Webhook and notification provider setup
- Architecture — API container design and data flow