Hosts & services

A watched domain is more than its apex. Every host below it (api., webmail., mx1.) serves a certificate of its own and answers, or stops answering, on its own. Hosts live under the domain, and every route here requires the domain to be verified first.

Scopes

monitor:read to read, monitor:write to change. Same key and same rules as watched domains.

Two quotas, both per domain

  • limit: certificate hosts in the inventory.
  • services_limit: service checks across all hosts of the domain. A paused profile costs nothing and takes no slot.

Both come back on every response of these routes, next to the current usage.

The host object

FieldTypeDescription
id integerUnique identifier of the host.
hostname stringA name strictly below the watched domain. The apex is covered by the tls_expiry check, not here.
source enumzone · ct · manual: where the host came from.
enabled booleanWhether the host is being checked.
status enumok · warn · fail · unknown, for the certificate.
not_after datetime | nullCertificate expiry (UTC).
days_left integer | nullDays until expiry; negative once expired.
issuer string | nullIssuer, RFC 4514 string.
sans string[]Subject alternative names on the served certificate.
chain_valid boolean | nullWhether the chain validated.
changed booleanThe certificate served now is not the one accepted as normal.
services object[]Service profiles switched on for this host. See below.

The service check object

FieldTypeDescription
profile enumweb · webmail · email: a value from the catalog.
status enumok · warn · fail · unknown. The worst of the profile's probes, with unknown above ok and below fail.
latency_ms integer | nullSlowest healthy probe of the last cycle.
probes object[] | nullOne entry per probe: target, protocol, ok, latency_ms, reason, detail, unknown.
first_ok_at datetime | nullWhen the service was first seen up. While null, a failure is state on the panel and never an alert.
last_checked_at datetime | nullLast measurement (UTC).
last_error string | nullWhy the last measurement failed, per protocol.

The profile catalog

ProfileTypeDescription
web HTTPS 443Website or API. Anything below 500 counts as up; TLS validation is off here on purpose, because the certificate is the inventory's job.
webmail HTTPS 443 + IMAP 993The login page and the IMAP behind it. A * OK or * PREAUTH banner is healthy.
email SMTP 25Incoming mail, read by the 220 banner, not just an open port.

There is no port field, and that is the design

A profile is one word from a closed catalog, never a form with protocol, port, path and expected status. Two reasons. Product: asking the account to configure is the no-editor version of asking it to program, and a mistyped port teaches people that the product cries wolf. Security: a port field that does not exist cannot be used to sweep someone else's network on our behalf. Add ownership of the domain and the refusal to probe anything that does not resolve to a public IP, and there is no attacker-chosen target here. A profile outside the catalog returns 422 service_profile_unknown.

We never sign in

Proving that IMAP delivers mail would need a real mailbox credential, and a monitor carrying a password becomes a target instead of a witness. The limit is known and stated: an IMAP that greets and rejects every login passes as healthy here.

Discover hosts

POST /orgs/{org_id}/watched-domains/{domain_id}/hosts/discover

Fills the inventory from your zone here (when the domain is also hosted with us) and from public CT logs, up to the quota, and switches on the service profile each new host most likely needs: being an MX of the domain wins, then the leftmost label, otherwise web. Hosts that already carry a profile, switched on or paused, are never touched, because that was somebody's decision.

cURL
curl -X POST "https://api.monitor.quathos.com/v1/orgs/$ORG_ID/watched-domains/17/hosts/discover" \
  -H "Authorization: Bearer $QUATHOS_KEY"
response · 200 OK
{
  "hosts": [ … ],
  "used": 4,
  "limit": 25,
  "services_used": 4,
  "services_limit": 15,
  "added": 4,
  "skipped_quota": 0,
  "skipped_unresolved": 2,
  "services_added": 4,
  "services_skipped_quota": 0
}

skipped_unresolved counts names that do not resolve to a public IP; those are not added and do not consume quota.

Add a host by hand

POST /orgs/{org_id}/watched-domains/{domain_id}/hosts

Body parameters

ParameterTypeDescription
hostname requiredstringA name strictly below the watched domain. Wildcards and the apex are rejected; the name must resolve to a public IP.
cURL
curl -X POST "https://api.monitor.quathos.com/v1/orgs/$ORG_ID/watched-domains/17/hosts" \
  -H "Authorization: Bearer $QUATHOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hostname": "api.example.com" }'

409 certificate_host_exists if the host is already there, 409 certificate_domain_unverified if the domain has not proven ownership, and 422 for an invalid name, a name outside the domain or one that does not resolve publicly.

List the inventory

GET /orgs/{org_id}/watched-domains/{domain_id}/hosts

Every host with its certificate, its service profiles and both quotas. Paused profiles are not listed: what comes back is what is being watched.

Switch a service on

POST /orgs/{org_id}/watched-domains/{domain_id}/hosts/{host_id}/services

Starts watching one profile on this host. The first probe runs on the next tick, not on the next plan cycle. Switching on a profile that is already on returns 409 service_check_exists; a previously paused one is simply resumed.

Body parameters

ParameterTypeDescription
profile requiredstringweb · webmail · email. Anything else returns 422: the catalog is closed.

Example

cURL
curl -X POST "https://api.monitor.quathos.com/v1/orgs/$ORG_ID/watched-domains/17/hosts/88/services" \
  -H "Authorization: Bearer $QUATHOS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "profile": "webmail" }'
response · 201 Created (excerpt)
{
  "hosts": [
    {
      "id": 88,
      "hostname": "webmail.example.com",
      "source": "ct",
      "enabled": true,
      "status": "ok",
      "days_left": 63,
      "issuer": "CN=R11,O=Let's Encrypt,C=US",
      "changed": false,
      "services": [
        {
          "profile": "webmail",
          "enabled": true,
          "status": "fail",
          "latency_ms": 118,
          "probes": [
            { "target": "https://webmail.example.com/", "protocol": "https", "ok": true,
              "latency_ms": 118, "reason": null, "detail": "200", "unknown": false },
            { "target": "webmail.example.com:993", "protocol": "imap", "ok": false,
              "latency_ms": null, "reason": "timeout", "detail": null, "unknown": false }
          ],
          "first_ok_at": "2026-09-12T12:04:00.312490",
          "last_checked_at": "2026-09-12T18:20:11.874120",
          "last_error": "imap: timeout"
        }
      ]
    }
  ],
  "used": 4,
  "limit": 25,
  "services_used": 5,
  "services_limit": 15
}

Switch a service off

DELETE /orgs/{org_id}/watched-domains/{domain_id}/hosts/{host_id}/services/{profile}

Pauses the profile and closes any alert it had opened. It stops probing, stops counting against the quota and disappears from the listing, but the decision is remembered, so the next discover will not switch it back on.

cURL
curl -X DELETE "https://api.monitor.quathos.com/v1/orgs/$ORG_ID/watched-domains/17/hosts/88/services/webmail" \
  -H "Authorization: Bearer $QUATHOS_KEY"

What reaches you as an alert

Alerts go out on e-mail and signed webhooks, one per (host, profile): service.web_unreachable, service.webmail_unreachable and service.email_unreachable. Certificates keep their own kinds (tls.expiring, tls.invalid, tls.cert_changed).

Three rules that keep the alert meaningful

A failure is probed again before it counts. Nothing alerts until the service has been seen up at least once: "stopped answering" presumes it answered. And when we cannot reach port 25 from here, we probe a third-party mail server too: if that one is silent as well, the honest answer is unknown (reason: "probe_blind"), not a claim that your server is down.