Certificates
Generated from core/api/router.go. Edit the router, not this file.
| Method | Path | Who can call it |
|---|---|---|
GET | /api/v1/certificates | Any authenticated user |
POST | /api/v1/certificates | Operator, admin |
GET | /api/v1/certificates/:id | Any authenticated user |
PATCH | /api/v1/certificates/:id | Operator, admin |
DELETE | /api/v1/certificates/:id | Admin |
GET | /api/v1/certificates/:id/private-key | Admin |
POST | /api/v1/certificates/:id/renew | Operator, admin |
POST | /api/v1/certificates/:id/revoke | Admin |
GET /certificates filters on status, environment, common_name, ca_account_id.
Issuing
POST /api/v1/certificates{
"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. 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.
On failure the record is marked RENEWAL_FAILED with renewal_error set, and the previous certificate is left intact.
Exporting a private key
GET /api/v1/certificates/:id/private-key{ "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.
Endpoint detail
GET /api/v1/certificates
| Who can call it | Any authenticated user |
| Handler | certHandler.List |
| Display token | Readable by an unattended screen |
Parameters
| Name | In | Default | |
|---|---|---|---|
ca_account_id | query | ||
common_name | query | ||
environment | query | ||
limit | query | ||
offset | query | ||
status | query |
Responses
| Status | Body |
|---|---|
200 | An object with data (Certificate[]), total (integer) |
400 | { "error": … } |
500 | { "error": … } |
Example request
curl -X GET 'https://certpilot.example.com/api/v1/certificates' \
-H 'Authorization: Bearer <token>'POST /api/v1/certificates
| Who can call it | Operator, admin |
| Handler | certHandler.Create |
| Display token | Refused — not a viewer-safe GET |
Request body
RequestCertificateInput defines the payload to request a new certificate.
| Field | Type | Description | |
|---|---|---|---|
common_name | string | CommonName is required unless CSRPEM is supplied, in which case the names come from the request itself. Validated below rather than by a binding tag, which cannot express "one of these two". | |
sans | string[] | ||
ca_account_id | string | required | |
csr_pem | string | CSRPEM 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_type | string | ||
key_size | integer | ||
validity_days | integer | ||
environment | string | ||
team | string | ||
auto_renew | boolean | ||
renewal_lead_days | integer | ||
metadata | object | Metadata holds values for the admin-defined fields. Required ones are enforced at request time. |
Responses
| Status | Body |
|---|---|
201 | An object with certificate (Certificate), policy_violations, warnings (string[]) |
400 | { "error": … } · common_name is required when no csr_pem is supplied |
403 | An object with violations · Certificate request blocked by security policy |
500 | { "error": … } |
502 | { "error": … } · gateway reported success but returned no certificate |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/certificates' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"ca_account_id": "<ca_account_id>",
"common_name": "<common_name>",
"sans": [
"<sans>"
],
"csr_pem": "<csr_pem>",
"key_type": "<key_type>"
}'GET /api/v1/certificates/:id
| Who can call it | Any authenticated user |
| Handler | certHandler.Get |
| Display token | Readable by an unattended screen |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
200 | A Certificate object |
404 | { "error": … } |
Example request
curl -X GET 'https://certpilot.example.com/api/v1/certificates/<id>' \
-H 'Authorization: Bearer <token>'PATCH /api/v1/certificates/:id
| Who can call it | Operator, admin |
| Handler | certHandler.UpdateMetadata |
| Display token | Refused — 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
| Name | In | Default | |
|---|---|---|---|
id | path | required |
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.
| Field | Type | Description |
|---|---|---|
environment | string | |
team | string | |
tags | string[] | |
metadata | object |
Responses
| Status | Body |
|---|---|
200 | A Certificate object |
400 | { "error": … } |
404 | { "error": … } |
500 | { "error": … } |
Example request
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 it | Admin |
| Handler | certHandler.Delete |
| Display token | Refused — not a viewer-safe GET |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
200 | An object with message (string) |
404 | { "error": … } · no such certificate |
409 | An object with not_after, status |
500 | { "error": … } |
Example request
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 it | Admin |
| Handler | certHandler.PrivateKey |
| Display token | Refused — not a viewer-safe GET |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
200 | An 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": … } |
Example request
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 it | Operator, admin |
| Handler | certHandler.Renew |
| Display token | Refused — not a viewer-safe GET |
Parameters
| Name | In | Default | |
|---|---|---|---|
id | path | required |
Responses
| Status | Body |
|---|---|
202 | An object with data (RenewalJob), message (string) |
400 | { "error": … } |
404 | { "error": … } |
500 | { "error": … } |
Example request
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 it | Admin |
| Handler | certHandler.Revoke |
| Display token | Refused — 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
| Name | In | Default | |
|---|---|---|---|
id | path | required |
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.
| Field | Type | Description | |
|---|---|---|---|
reason | integer | required |
Responses
| Status | Body |
|---|---|
200 | An object with certificate (Certificate), message |
400 | { "error": … } |
404 | { "error": … } · no such certificate |
409 | An object with revoked_at · this certificate is already revoked |
500 | { "error": … } |
502 | { "error": … } · the CA account this certificate was issued by could not be read |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/certificates/<id>/revoke' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"reason": 0
}'