Skip to content

Certificates ​

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

MethodPathWho can call it
GET/api/v1/certificatesAny authenticated user
POST/api/v1/certificatesOperator, admin
GET/api/v1/certificates/:idAny authenticated user
PATCH/api/v1/certificates/:idOperator, admin
DELETE/api/v1/certificates/:idAdmin
GET/api/v1/certificates/:id/private-keyAdmin
POST/api/v1/certificates/:id/renewOperator, admin
POST/api/v1/certificates/:id/revokeAdmin

GET /certificates filters on status, environment, common_name, ca_account_id.

Despite its name, common_name matches any of a certificate's names, the subject common name or any SAN, by substring and ignoring case. Conforming CAs are dropping the common name (Pebble already issues without one), and a certificate whose only name is in its SANs must still be findable by it. A certificate matches once however many of its names do.

Issuing ​

http
POST /api/v1/certificates
json
{
  "common_name": "app.example.com",
  "sans": ["www.app.example.com"],
  "ca_account_id": "uuid",
  "key_type": "ECDSA",
  "key_size": 256,
  "validity_days": 90,
  "environment": "production",
  "team": "platform",
  "auto_renew": true,
  "renewal_lead_days": 30
}

What happens, in order: the CA account is loaded, policy is evaluated, the gateway is located, its stored configuration is decrypted for exactly this one call, issuance is requested, and the returned certificate is parsed before it is stored. A gateway that returns anything other than a valid X.509 certificate produces a 502 rather than a record marked ISSUED.

Metadata on the stored record — serial, issuer, validity dates, fingerprint, key type and size — is read from the certificate itself, not from what the gateway claimed.

If the gateway returns a private key, it is sealed before it touches the database. Supplying your own CSR avoids this entirely and is the better pattern: the key then stays wherever it was generated.

Returns 201. When non-blocking policy violations were recorded, the body is {"certificate": {...}, "policy_violations": [...]} instead of the bare record — a policy that finds something must say so even when it does not block.

Renewing ​

POST /certificates/:id/renew runs the same path, and asks the CA for the same lifetime: the template's validity_days, or the certificate's own when the template names none. Before #102 a renewal asked for nothing, and a 90-day certificate could come back lasting a year. The key is rotated and the new one persisted; a renewal that produced a certificate without storing its matching key would leave a record that looks healthy and cannot terminate TLS. That is only possible for a key CertPilot already holds: a certificate whose key_custody is AGENT or EXTERNAL is refused with 400, and renewed by whoever holds its key (see Asking for one).

On failure the record is marked RENEWAL_FAILED with renewal_error set, and the previous certificate is left intact.

Exporting a private key ​

http
GET /api/v1/certificates/:id/private-key
json
{ "common_name": "app.example.com", "private_key_pem": "-----BEGIN..." }

Admin only. Writes cert.private_key_exported to the audit log with the actor and client IP. Returns 404 when no key is stored — which is the normal case for imported, discovered, or CSR-based certificates.

Revoking ​

http
POST /api/v1/certificates/:id/revoke
json
{ "reason": 1 }

Admin only. The CA is told first, and the record is updated only if the CA agreed. The ordering is the guarantee: a record reading REVOKED beside a certificate that still answers handshakes is worse than either alternative, so every failure on this path leaves the record untouched and says so.

reason is required. Accepted codes are 0 unspecified, 1 keyCompromise, 3 affiliationChanged, 4 superseded, 5 cessationOfOperation, 9 privilegeWithdrawn — the subset of RFC 5280 a CA will act on. Anything else is a 400 listing them, so a caller does not have to go and read the RFC.

StatusWhen
200Revoked at the CA and recorded
400Missing or unaccepted reason; no stored copy of the certificate; or no CA account to send it to
404No such certificate
409Already revoked — the CA is not asked again
502Gateway not connected, or the CA declined. Nothing has been changed
500The CA revoked it and the record could not be written. The certificate is dead and the record is wrong until the write succeeds

Writes certificate.revoked to the audit log with the actor, reason code and fingerprint, and publishes cert.revoked on the event stream so a wall display reflects it without waiting for a sweep.

DELETE /certificates/:id is not a substitute: it removes the record and the certificate stays valid at the CA. It refuses a live certificate with a 409 that names this endpoint; ?forget=true overrides that deliberately.

Endpoint detail ​

GET /api/v1/certificates ​

Who can call itAny authenticated user
HandlercertHandler.List
Display tokenReadable by an unattended screen

Parameters ​

NameInDefault
ca_account_idquery
common_namequery
environmentquery
limitquery
offsetquery
statusquery

Responses ​

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

POST /api/v1/certificates ​

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

Request body ​

RequestCertificateInput defines the payload to request a new certificate.

FieldTypeDescription
common_namestringCommonName is required unless CSRPEM is supplied, in which case the names come from the request itself. Checked by the resolver rather than by a binding tag, which cannot express "one of these two".
sansstring[]
template_idstringTemplateID names the certificate template to issue under, by slug or by uuid. A slug is what a pipeline should carry: it survives a rename. Optional, for now. A request without one resolves to the default template for CAAccountID, which constrains nothing — so every caller written before templates existed behaves exactly as it did. Requiring it is the third stage of #25 and is a decision for an operator, not a migration.
ca_account_idstringCAAccountID is only consulted when TemplateID is absent. A template pins its own issuer, so naming both is naming the same thing twice — and when they disagree the template wins, because a requester who could choose an issuer could choose the cheapest, the least logged, or the one with the widest trust. No longer binding:"required": one of the two has to be present, which a binding tag cannot express. The resolver says which is missing.
csr_pemstringCSRPEM is a certificate signing request whose private key was generated somewhere else and never sent here. This is the path for the keys CertPilot must not hold: an HSM, a load balancer that generates its own, a team whose policy forbids a key leaving their host. The names, key type and key size are taken from the request and the corresponding fields above are ignored, because the only key that can serve the certificate is the one the requester already has — honouring a conflicting key_type here would issue a certificate nobody can use.
key_typestring
key_sizeinteger
validity_daysinteger
environmentstring
teamstring
auto_renewboolean
renewal_lead_daysinteger
metadataobjectMetadata holds values for the admin-defined fields. Required ones are enforced at request time.

Responses ​

StatusBody
201An object with certificate (Certificate), policy_violations, warnings (string[])
400{ "error": … }
500{ "error": … }
502An object with conformance_findings
· gateway reported success but returned no certificate
Example request
bash
curl -X POST 'https://certpilot.example.com/api/v1/certificates' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "common_name": "<common_name>",
  "sans": [
    "<sans>"
  ],
  "template_id": "<template_id>",
  "ca_account_id": "<ca_account_id>",
  "csr_pem": "<csr_pem>"
}'

GET /api/v1/certificates/:id ​

Who can call itAny authenticated user
HandlercertHandler.Get
Display tokenReadable by an unattended screen

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
200A Certificate object
404{ "error": … }
500{ "error": … }
· could not read the certificate
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/certificates/<id>' \
  -H 'Authorization: Bearer <token>'

PATCH /api/v1/certificates/:id ​

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

The only mutable part of a certificate: the decisions people record about it, not the facts the CA established.

Parameters ​

NameInDefault
idpathrequired

Request body ​

UpdateMetadataInput is what an operator may change on a certificate. Pointers, so an omitted field is left alone rather than cleared. A form that edits only the team must not blank the environment, and a client that knows about three of these fields must not erase the fourth.

FieldTypeDescription
environmentstring
teamstring
tagsstring[]
metadataobject

Responses ​

StatusBody
200A Certificate object
400{ "error": … }
404{ "error": … }
500{ "error": … }
· could not read the certificate
Example request
bash
curl -X PATCH 'https://certpilot.example.com/api/v1/certificates/<id>' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "environment": "<environment>",
  "team": "<team>",
  "tags": [
    "<tags>"
  ],
  "metadata": {}
}'

DELETE /api/v1/certificates/:id ​

Who can call itAdmin
HandlercertHandler.Delete
Display tokenRefused — not a viewer-safe GET

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
200An object with message (string)
404{ "error": … }
· no such certificate
409An object with not_after, status
500{ "error": … }
· could not read the certificate
Example request
bash
curl -X DELETE 'https://certpilot.example.com/api/v1/certificates/<id>' \
  -H 'Authorization: Bearer <token>'

GET /api/v1/certificates/:id/private-key ​

Who can call itAdmin
HandlercertHandler.PrivateKey
Display tokenRefused — not a viewer-safe GET

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
200An object with common_name, private_key_pem
404{ "error": … }
· no private key is stored for this certificate — it was either imported, discovered, or issued from a CSR whose key never left its host
500{ "error": … }
· could not read the certificate
Example request
bash
curl -X GET 'https://certpilot.example.com/api/v1/certificates/<id>/private-key' \
  -H 'Authorization: Bearer <token>'

POST /api/v1/certificates/:id/renew ​

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

Parameters ​

NameInDefault
idpathrequired

Responses ​

StatusBody
202An object with data (RenewalJob), message (string)
400{ "error": … }
404{ "error": … }
500{ "error": … }
· could not read the certificate
Example request
bash
curl -X POST 'https://certpilot.example.com/api/v1/certificates/<id>/renew' \
  -H 'Authorization: Bearer <token>'

POST /api/v1/certificates/:id/revoke ​

Who can call itAdmin
HandlercertHandler.Revoke
Display tokenRefused — not a viewer-safe GET

Exporting a private key is admin-only and audited: it is the one operation that removes a secret from the system's custody. Revocation is admin, and it is the operation DELETE was being used for. It tells the CA first and records only what the CA accepted, so a certificate can never read REVOKED here while still answering handshakes in production.

Parameters ​

NameInDefault
idpathrequired

Request body ​

RevokeCertificateInput names the reason. It is required rather than defaulted: "unspecified" is a legitimate answer, but it should be one somebody chose, because the reason is what tells the next reader whether a key was compromised or a service was simply retired.

FieldTypeDescription
reasonintegerrequired

Responses ​

StatusBody
200An object with certificate (Certificate), message
400{ "error": … }
404{ "error": … }
· no such certificate
409An object with revoked_at
· this certificate is already revoked
500{ "error": … }
· could not read the certificate
502{ "error": … }
Example request
bash
curl -X POST 'https://certpilot.example.com/api/v1/certificates/<id>/revoke' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "reason": 0
}'