API
REST API for certificate monitoring: add, delete and re-check domains, read health scores and history. Token auth, JSON in and out, from Starter up.
Use the REST API to do from a script what you would otherwise do in the browser: add and remove
domains, start checks, read health scores. Available on Starter and Pro under /api/v1/.
Everything is JSON, in and out.
Get a token
- Go to Settings → API token and copy the token.
- Send it in the
Authentication-Tokenheader with every request:
curl https://certwatchr.com/api/v1/domains \
-H 'Authentication-Token: <YOUR-TOKEN>'
Treat the token like a password: whoever has it can add, remove and read your domains. If it ends up in a log, a CI output or a screenshot, click Rotate token. The old token stops working at once, so update your scripts right away.
Rotating does not sign you out of the web app, and changing your password does not change the token.
On a team, every member has their own token for the team's domains. When someone leaves, their token stops working there at once — there is no shared token to rotate.
Domains
GET /api/v1/domains # list your domains
POST /api/v1/domains # add a domain
DELETE /api/v1/domains/<id> # remove a domain
POST /api/v1/domains/<id>/check # check the certificate now
Add a domain
curl -X POST https://certwatchr.com/api/v1/domains \
-H 'Authentication-Token: <YOUR-TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"hostname":"example.com","port":443,"alert_at_days":30,"tags":["prod","web"]}'
| Field | Required | Notes |
|---|---|---|
hostname |
yes | Up to 255 characters. Lowercased and trimmed for you. |
port |
no | 1–65535. Default 443. |
label |
no | Up to 128 characters. |
alert_at_days |
no | 1–365, adjusted to your plan's range; always 30 on Free. Default 30. |
tags |
no | A list (["prod","web"]) or a string ("prod,web"). |
Adding a hostname and port you already monitor returns 409 Conflict and changes nothing, so you can safely run a script twice. Your plan's domain limit applies, as in the browser.
What a domain looks like
{
"id": 42,
"hostname": "example.com",
"port": 443,
"label": "Main site",
"tags": ["prod", "web"],
"alert_at_days": 30,
"status": "ok",
"days_remaining": 67,
"cert_expiry": "2026-11-28T09:14:00",
"cert_issuer": "Let's Encrypt",
"cert_subject": "example.com",
"last_checked_at": "2026-09-22T06:00:12",
"alerts_snoozed_until": null
}
status is untested, ok, warning, critical or error. POST .../check answers
202 {"queued": true}; the result shows up on the domain a few seconds later.
Health score
GET /api/v1/domains/<id>/health # latest score, breakdown, suggestions
POST /api/v1/domains/<id>/health/check # run a health check now
GET /api/v1/domains/<id>/health/history?limit=90 # score over time, oldest first
GET /health returns the last result; it does not start a new check:
{
"health_score": 90,
"breakdown": {
"chain_trusted": 20,
"hostname_match": 15,
"not_yet_valid": 5,
"expiry_proximity": 20,
"revocation": 10,
"chain_complete": 0,
"tls_version": 10,
"weak_algorithms": 10
},
"certificate": {
"subject": "example.com",
"issuer": "Let's Encrypt",
"days_remaining": 67
},
"errors": ["Incomplete chain: server did not send its intermediate certificate"],
"suggestions": [
{
"category": "chain_complete",
"points": 10,
"severity": "critical",
"title": "Send the intermediate certificates too",
"why": "..."
}
],
"last_health_checked_at": "2026-09-22T06:30:00"
}
The categories and points are explained on the Health score page.
suggestions is the same "what to fix" list as on the domain page, most urgent first.
history returns one entry per check with health_score, breakdown, tls_version,
signature_algorithm, errors and checked_at. limit is 90 by default, at most 365.
Notifications
The same list as the bell in the header — the bell itself loads it from here. In your own language, with the link each entry points to.
GET /api/v1/notifications/count # unread entries, and whether one is critical
GET /api/v1/notifications # what is still open, most urgent first
GET /api/v1/notifications?view=all # everything, newest first
POST /api/v1/notifications/<id>/read # mark one read
POST /api/v1/notifications/read-all # mark all read
{
"unread": 1, "critical": false, "locked": false, "view": "open",
"entries": [{
"id": 42, "kind": "cert_changed", "severity": "info", "group": "certificate",
"title": "New certificate on example.com",
"text": "A different certificate is served than at the last check.",
"link": "/domains/7", "domain_id": 7,
"created_at": "2026-11-03T08:00:00", "updated_at": "2026-11-03T08:00:00",
"updated_display": "2026-11-03 09:00", "read": false, "resolved": false
}]
}
limit is 20 by default, at most 100. Times are UTC; updated_display is the same time as the
web app prints it. Both read calls answer with the new unread / critical counts. Reading an
info entry also closes it; warnings stay open until the problem is fixed. On Free, locked is
true and the list is empty. See Notification centre.
Errors
Every error is JSON with a matching HTTP status — never an HTML page or a redirect to the login form:
{"error": "Conflict", "message": "example.com:443 is already being monitored."}
| Status | Meaning |
|---|---|
400 |
Invalid JSON, or a field failed validation. The message says which. |
401 |
Authentication-Token missing or invalid. |
403 |
Your plan does not include this, the domain is not yours, or you reached your domain limit. |
404 |
No such domain or notification. |
409 |
You already monitor that hostname and port. |
Retry 5xx errors after a short pause. 4xx errors stay the same until you change the request.