CertHaloSSL expiry monitoring · by IncubXperts
Docs

API and webhooks

Add domains from your deploy pipeline, read certificate results from your own tools, and get each alert as a signed request to your own server. Available on the Pro and Business plans.

API keys

The workspace's Owner and Admins create keys on the Developers page of the dashboard. A key is read-only or read and write, works for one workspace, and is shown once: store it as a secret in your CI. Revoking it stops it at once.

Send it with every request:

curl -H "Authorization: Bearer $CERTHALO_API_KEY" https://certhalo.com/api/v1/domains

Endpoints

Base URL https://certhalo.com/api/v1. A domain is written as on the dashboard, example.com or example.com:8443, URL-encoded in paths. Responses are JSON. The full description is in OpenAPI format for Postman, Insomnia or code generators.

RequestKeyWhat it does
GET /domainsAnyEvery domain in the workspace: status, grade, expiry date, days left, issuer, last and next check.
POST /domainsRead and writeAdds a domain and runs its first check. Body: {"domain": "example.com"}. Your plan's limit applies.
GET /domains/{domain}AnyOne domain with its latest full result: certificate, chain, findings, revocation and TLS versions.
DELETE /domains/{domain}Read and writeStops monitoring it and deletes its check history.
GET /domains/{domain}/historyAnyIts checks, newest first, for the last 90 days. ?limit=1…500 (default 100).
POST /domains/{domain}/checkRead and writeChecks it now and returns the new result. Once every 5 minutes per domain.

A domain, as listed

{
  "domain": "shop.example.com",
  "host": "shop.example.com",
  "port": 443,
  "status": "ok",               // pending, ok, warning, critical or error
  "grade": "A",
  "validTo": "2027-01-14T23:59:59.000Z",
  "daysRemaining": 77,
  "issuer": "R11",
  "lastCheckedAt": "2026-10-29T08:59:48.624Z",
  "nextCheckAt": "2026-10-29T14:59:48.624Z",
  "error": null                 // why the last check failed, if it did
}

A single domain adds result: the full result of its latest check.

Errors and limits

Errors look like {"error": {"code": "not_found", "message": "…"}}:

Example: GitHub Actions

Add a new site to monitoring when it's deployed, then check it straight away (a read and write key, stored as the secret CERTHALO_API_KEY):

- name: Monitor the certificate with CertHalo
  env:
    CERTHALO_API_KEY: ${{ secrets.CERTHALO_API_KEY }}
  run: |
    # 201 when added, 422 when it's already monitored: both are fine here.
    curl -sS -X POST https://certhalo.com/api/v1/domains \
      -H "Authorization: Bearer $CERTHALO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"domain": "app.example.com"}'
    curl -sS -X POST https://certhalo.com/api/v1/domains/app.example.com/check \
      -H "Authorization: Bearer $CERTHALO_API_KEY"

Webhooks

Connect a webhook under Connect a channel on the Alerts page, next to Slack and Microsoft Teams. CertHalo then sends each alert as a POST with a JSON body. Like a chat channel, it gets Everything or Urgent only. The URL must use HTTPS and answer with a 2xx status within 5 seconds; a timeout or 5xx is retried once, and after 5 failed deliveries in a row (or a 404 or 410) the webhook is turned off and the Owner and Admins are emailed.

POST /certhalo HTTP/1.1
Content-Type: application/json
CertHalo-Event: certificate.expiring
CertHalo-Delivery: evt_3f6c0f1c9a2b4d5e8f7a6b5c4d3e2f1a
CertHalo-Signature: t=1793289599,v1=5d1c…

{
  "id": "evt_3f6c0f1c9a2b4d5e8f7a6b5c4d3e2f1a",
  "type": "certificate.expiring",
  "createdAt": "2026-10-29T15:29:59.000Z",
  "workspace": { "id": "…", "name": "Acme IT Ops" },
  "domain": "shop.example.com",
  "data": { "daysRemaining": 7, "validTo": "2026-11-05T23:59:59.000Z" },
  "summary": { "title": "shop.example.com: SSL certificate expires in 7 days", "detail": "…" },
  "reportUrl": "https://certhalo.com/check/shop.example.com"
}

Events

certificate.expiringA reminder before expiry (on your reminder days)
certificate.expiredThe certificate has expired
certificate.renewedA new certificate with a later expiry date
certificate.issuer_changedA certificate from a different issuer
certificate.revokedThe issuer revoked it
certificate.untrustedThe chain no longer leads to a trusted root
certificate.chain_needs_fixBrowsers repair the chain, but apps and APIs can't
certificate.hostname_mismatchThe certificate doesn't cover the domain
domain.unreachableTwo checks in a row couldn't connect
domain.recoveredReachable again
testSent when you connect a webhook or press “Send a test”

Use CertHalo-Delivery to ignore an event you've already handled. data holds what the event is about: daysRemaining, validTo, previousValidTo, issuer, previousIssuer, revokedAt or reason, when they apply.

Checking the signature

Each webhook has its own secret (whsec_…), shown once when you connect it; New secret on the Alerts page replaces it. The signature is an HMAC-SHA256 of the timestamp, a full stop and the raw request body. Check it against the raw body, before parsing the JSON, and reject timestamps more than 5 minutes old.

// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCertHalo(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
# Python
import hashlib, hmac, time

def verify_certhalo(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Questions or something missing? Email support@certhalo.com. Not on Pro or Business yet? See the plans.