StrategyK Infrastructure

Site Monitor API

Register a website and it is checked every 60 seconds — status code, page content, response time, TLS certificate expiry and DNS resolution. Two consecutive failures raise an alert by email and phone push.

Base URLhttps://monitor.strategyk.com.au
Auth headerX-API-Key
Content typeapplication/json
Runs onEC2 · ap-southeast-2

Getting started

Every request needs the X-API-Key header. Adding a site takes one call — the only required fields are slug, name and url.

curl -X POST https://monitor.strategyk.com.au/api/v1/sites \
  -H "X-API-Key: $MONITOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "acme-corp",
    "name": "Acme Corp",
    "url":  "https://acmecorp.com.au",
    "group": "clients",
    "expect": {
      "body_contains": ["Welcome to Acme"]
    }
  }'

Changes take about 10 seconds to take effect. The API writes the check definition to disk; a separate watcher then reloads the checking engine. A 201 means the site was accepted and stored — the first check result follows shortly after.

Always send body_contains

A status code alone is a weak signal. A hacked site, an expired CMS licence, a blank page after a bad deploy and a "coming soon" parking page all return 200 OK. Pass a short, stable phrase that only appears when the page has genuinely rendered — a headline, a product name, a phone number.

Avoid anything that changes: dates, prices, cart counts, CSRF tokens, or a year in a copyright line.

Add or update a site

POST/api/v1/sites

Creates the site, or replaces it entirely if the slug already exists. This is an upsert — there is no separate update call, and a repeat POST is safe.

Returns 201 with:

{"status": "written", "slug": "acme-corp", "note": "gatus reloads within ~10s"}

Replaces, not merges. Fields you leave out revert to their defaults. To change one setting, GET the site first, modify the object, and POST the whole thing back.

List all sites

GET/api/v1/sites

Returns every registered site as stored, including defaults that were filled in for you. Use this to confirm what the monitor actually holds.

{"sites": [{"slug": "acme-corp", "name": "Acme Corp", "url": "https://acmecorp.com.au/", ...}]}

Fetch one site

GET/api/v1/sites/{slug}

Returns the full stored object, or 404 if the slug is unknown. This is the object to modify and POST back when editing.

Remove a site

DELETE/api/v1/sites/{slug}

Stops checking immediately and deletes the definition. Returns 200, or 404 if the slug was not registered.

{"status": "deleted", "slug": "acme-corp"}

Replace the whole list

PUT/api/v1/sites

Declares the complete set of sites. Anything present in the monitor but absent from the payload is removed. Returns the slugs written and removed.

{"status": "reconciled",
 "written": ["acme-corp", "beta-ltd"],
 "removed": ["old-client"]}

This is the drift-correction call. Because the monitor keeps its own copy of the list, it stays up even when FullStack+ is down — but the two can diverge. Sending the authoritative list here on a schedule (nightly is plenty) re-synchronises them.

This deletes. A partial list silently stops monitoring every site you left out. Only call it with the complete set — for single changes use POST or DELETE.

Current health

GET/api/v1/status

Live results for every check. Use this to surface site health inside FullStack+ without duplicating any checking logic.

{
  "total": 28,
  "unhealthy": 1,
  "endpoints": [
    {
      "name": "acme-corp",
      "group": "clients",
      "healthy": false,
      "last_checked": "2026-08-30T11:52:04Z",
      "response_time_ms": 4820,
      "errors": ["[BODY] == pat(*Welcome to Acme*)"]
    }
  ]
}

Each site produces two entries: the HTTP check under its slug, and the DNS check as {slug}-dns. The errors array names the specific assertions that failed, so you can show why a site is down, not just that it is.

Service health

GET/healthz

The only unauthenticated endpoint. Returns {"ok": true, "sites": 13}. Use it for your own uptime checks on the monitor itself.

The site object

Only slug, name and url are required. Everything else has a working default.

FieldTypeDefaultNotes
slugstringYour stable identifier. Lowercase letters, digits and hyphens, 1–62 characters. Also the key for updates and deletes.
namestringHuman-readable label. Appears in alert emails and push notifications.
urlstringFull URL including scheme. Redirects are followed.
groupstringnullFree-text grouping, e.g. clients.
interval_secondsinteger60Between 20 and 86400.
enabledbooleantruefalse keeps the record but stops checking — use for a site that is intentionally offline.
check_dnsbooleantrueAdds a separate DNS resolution check for the hostname.
expectobjectsee belowWhat a healthy response looks like.
alertsobjectsee belowWhich channels to notify.

expect

FieldTypeDefaultNotes
statusinteger200Expected HTTP status after redirects.
body_containsstring[][]All must be present. This is the check that catches a broken site returning 200.
body_not_containsstring[]8 error stringsNone may be present. Defaults cover database errors, PHP fatals, WordPress critical errors and common gateway errors.
max_response_time_msinteger5000Slower than this counts as a failure.
min_cert_daysinteger14Alerts while there is still time to renew. HTTPS only.
min_body_bytesinteger512Catches blank pages and parking pages that return 200.

No asterisks in match strings. * is the wildcard character in the underlying matcher, so a literal one would silently widen your assertion. Sending one returns 422 rather than accepting a check that does not mean what you wrote.

alerts

FieldDefaultNotes
emailtrueSent via Postmark to gavin@strategyk.com.au.
pushtrueSimplePush notification to Gavin's phone.
smsfalseReserved for Ozetel SMS. Accepted now, inert until credentials are configured.

What gets checked

Every site gets the full battery on each run. A single failed assertion fails the check.

CheckCatches
DNS resolutionDomain stopped resolving, nameserver change gone wrong, expired domain
TLS handshakeCertificate invalid, misconfigured, or wrong hostname
Certificate expiryRenewal failed — warns 14 days ahead by default
HTTP status500s, unexpected redirects, origin down
Response timeDegradation before it becomes an outage
Body sizeBlank page, parking page, failed build
Content presentPage rendered but the real content is missing
Content absentDatabase errors and PHP fatals served with a 200

Alerting

Alerts fire after two consecutive failures, not one. At a 60-second interval that means roughly two minutes of genuine trouble — enough to ignore a single dropped request without meaningfully delaying a real alert.

  • Recovery notices are sent too. Two consecutive successes clear the alert, so you always learn when something came back.
  • Email goes direct from the checking engine, independently of this API — so alerts still arrive if the API container is down.
  • Push and SMS fan out through the API, which is where new channels get added.

Two permanent canaries run alongside your sites. One watches this API; the other checks the monitor's own internet access. If the egress canary fails at the same time as a wave of client sites, the fault is at the monitor's end, not at the sites — check that first before treating it as a mass outage.

Errors

CodeMeaning
200Success — list, fetch, delete, reconcile
201Site created or replaced
401Missing or wrong X-API-Key
404Unknown slug
422Validation failed — the response body names the offending field
502Checking engine unreachable (/status only)

Common 422 causes: a slug with uppercase letters or underscores, an interval_seconds outside 20–86400, a URL without a scheme, or an asterisk in a body match string.

Operations

For the StrategyK side, not for FullStack+.

  • Lives at /srv/containers/site-monitor on the EC2 container server. Three containers: api, gatus, reloader.
  • The internet-facing API has no Docker socket. The reloader holds it and has no network at all.
  • A malformed check definition would stop all monitoring, so the reloader verifies the engine restarted and rolls the change back if it did not — recovery measured at 21 seconds.
  • Rotate the key by editing MONITOR_API_KEY in .env and running docker compose up -d.
  • Check history is kept in SQLite at ./data/gatus.db.