Skip to content

Certificate Transparency

Generated from core/api/router.go. Edit the router, not this file.

MethodPathWho can call it
GET/api/v1/ct/certificatesAny authenticated user
GET/api/v1/ct/monitorsAny authenticated user
POST/api/v1/ct/monitorsOperator, admin
PUT/api/v1/ct/monitors/:idOperator, admin
DELETE/api/v1/ct/monitors/:idAdmin
POST/api/v1/ct/monitors/:id/checkOperator, admin

This is the half of discovery a network scan cannot reach. A scan answers "what is being served on the addresses I told you about". CT answers what has been issued in your name at all — by any CA, to anyone, whether or not it was ever deployed and whether or not the machine is reachable from CertPilot. A developer who obtained a certificate for api.corp.example.com with a personal ACME account appears in no scan of any range, and appears in CT within minutes, because every publicly-trusted CA is required to log there.

json
POST /api/v1/ct/monitors
{ "domain": "example.com", "include_subdomains": true, "check_interval_minutes": 360 }

Give the domain itself — *.example.com is refused, because subdomains are what include_subdomains is for. The floor on the interval is 60 minutes, higher than a scan's, for a different reason: the logs are read through a free community service, and polling it harder is how an organisation loses access to it and then finds out nothing at all.

Checked, versus answered

Every monitor carries both last_checked_at and last_success_at, and the list surfaces stale_domains derived from them.

Collapsing those into one field is the failure this whole product exists to prevent, in miniature: a monitor that has been unable to reach the log for a week would look exactly like a monitor that has found nothing for a week, and one of those means nobody is being told about certificates issued in their name. For the same reason POST …/check answers 502 when the index is unreachable, with its own words and the sentence "The check did not complete, so nothing was learned about this domain. It is not the same as finding no certificates."

One certificate, two log entries

A precertificate and its final certificate are logged separately and share a serial number. The index publishes no field saying which is which, so the earlier entry for a serial is labelled is_precertificate — precertificates are always logged first. Both rows are kept, because "this was pre-logged then issued" is real information, but GET /ct/certificates hides the pre-issuance row by default and the counts are per certificate.

This is not cosmetic. Before it existed, a live check of badssl.com reported 18 certificates where there are 9, and a headline number wrong by a factor of two is one people act on. Pass include_precertificates=true for the raw log entries.

Matching your own certificates

Findings are matched to inventory on serial number, since the log index does not publish a SHA-256 fingerprint. The two sides format serials differently — CertPilot stores unpadded lowercase hex, indexes pad and upper-case them — so both are normalised before comparison. Getting that wrong would report this system's own certificates as ones nobody manages, and a findings list full of your own certificates is one nobody reads.

An unmanaged finding publishes ct.unmanaged (WARNING), and only for certificates that are new to that monitor. Check windows overlap by design, and an alert repeating the same certificate every six hours is how a channel gets muted — taking the CA expiry alerts sharing it along with it.

Cloud inventory (ACM, Azure Key Vault, GCP, Kubernetes secrets) is not implemented yet.

Endpoint detail

GET /api/v1/ct/certificates

Who can call itAny authenticated user
HandlerctHandler.ListCertificates
Display tokenReadable by an unattended screen

Parameters

NameInDefault
include_precertificatesqueryfalse
limitquery100
management_statequery
monitor_idquery
offsetquery0

Responses

StatusBody
200An object with data (CTCertificate[]), total (integer)
500{ "error": … }
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/ct/certificates' \
  -H 'Authorization: Bearer <token>'

GET /api/v1/ct/monitors

Who can call itAny authenticated user
HandlerctHandler.ListMonitors
Display tokenReadable by an unattended screen

The half of discovery a network scan cannot reach: what has been issued in your name, whether or not it was ever deployed.

Responses

StatusBody
200An object with data (CTMonitor[]), min_interval_minutes, stale_domains, total, warning
500{ "error": … }
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/ct/monitors' \
  -H 'Authorization: Bearer <token>'

POST /api/v1/ct/monitors

Who can call itOperator, admin
HandlerctHandler.CreateMonitor
Display tokenRefused — not a viewer-safe GET

Request body

FieldTypeDescription
domainstringrequired
include_subdomainsboolean
is_enabledboolean
check_interval_minutesintegerCheckIntervalMinutes defaults to six hours. Certificates appear in the logs within minutes of issuance, but nobody acts on this within minutes, and the index is a service somebody else pays for.

Responses

StatusBody
201An object with data (CTMonitor), next
400{ "error": … }
409{ "error": … }
Example request
bash
curl -X POST 'https://certpilot.example.com/api/v1/ct/monitors' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "domain": "<domain>",
  "include_subdomains": false,
  "is_enabled": false,
  "check_interval_minutes": 0
}'

PUT /api/v1/ct/monitors/:id

Who can call itOperator, admin
HandlerctHandler.UpdateMonitor
Display tokenRefused — not a viewer-safe GET

Parameters

NameInDefault
idpathrequired

Request body

FieldTypeDescription
domainstringrequired
include_subdomainsboolean
is_enabledboolean
check_interval_minutesintegerCheckIntervalMinutes defaults to six hours. Certificates appear in the logs within minutes of issuance, but nobody acts on this within minutes, and the index is a service somebody else pays for.

Responses

StatusBody
200An object with data (CTMonitor)
400{ "error": … }
404{ "error": … }
500{ "error": … }
Example request
bash
curl -X PUT 'https://certpilot.example.com/api/v1/ct/monitors/<id>' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "domain": "<domain>",
  "include_subdomains": false,
  "is_enabled": false,
  "check_interval_minutes": 0
}'

DELETE /api/v1/ct/monitors/:id

Who can call itAdmin
HandlerctHandler.DeleteMonitor
Display tokenRefused — not a viewer-safe GET

Parameters

NameInDefault
idpathrequired

Responses

StatusBody
200An object with message
404{ "error": … }
500{ "error": … }
Example request
bash
curl -X DELETE 'https://certpilot.example.com/api/v1/ct/monitors/<id>' \
  -H 'Authorization: Bearer <token>'

POST /api/v1/ct/monitors/:id/check

Who can call itOperator, admin
HandlerctHandler.CheckMonitor
Display tokenRefused — not a viewer-safe GET

Parameters

NameInDefault
idpathrequired

Responses

StatusBody
200An object with data (CTMonitor), found (CTCertificate[]), summary, total (integer)
404{ "error": … }
500{ "error": … }
502An object with data (CTMonitor), message
Example request
bash
curl -X POST 'https://certpilot.example.com/api/v1/ct/monitors/<id>/check' \
  -H 'Authorization: Bearer <token>'