The agent API
Generated from core/api/router.go. Edit the router, not this file.
| Method | Path | Who can call it |
|---|---|---|
POST | /api/v1/agent/certificates | Enrolled agent |
POST | /api/v1/agent/deployments/claim | Enrolled agent |
POST | /api/v1/agent/deployments/result | Enrolled agent |
POST | /api/v1/agent/enrol | Enrolment token |
POST | /api/v1/agent/heartbeat | Enrolled agent |
POST | /api/v1/agent/installations | Enrolled agent |
POST | /api/v1/agent/inventory | Enrolled agent |
POST /api/v1/agent/certificates
| Who can call it | Enrolled agent |
| Handler | agentHandler.RequestCertificate |
The point of the agent: the host generated the key, and sends only a request. CertPilot signs what an operator granted this host and never holds a private key it could lose or be compelled to produce.
Request body
Request is what an agent submits.
| Field | Type | Description |
|---|---|---|
csr_pem | string | CSRPem carries the names, the public key, and a signature proving the requester holds the private half. Nothing else in it is honoured. |
install_path | string | InstallPath is where the agent intends to keep it, recorded so the inventory can be matched against issuance without guessing. |
renews | string | Renews is the certificate this replaces, when the agent is rotating a key it already holds. |
Responses
| Status | Body |
|---|---|
201 | An object with data |
400 | { "error": … } |
403 | An object with code (string) |
404 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/certificates' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>' \
-H 'Content-Type: application/json' \
-d '{
"csr_pem": "<csr_pem>",
"install_path": "<install_path>",
"renews": "<renews>"
}'POST /api/v1/agent/deployments/claim
| Who can call it | Enrolled agent |
| Handler | agentHandler.ClaimDeployments |
The inversion that makes an agent a deployment target. Every other target is deployed to by a core worker opening a connection; a host behind two firewalls claims the job itself, off the same queue, with the same lease and the same retry curve.
Responses
| Status | Body |
|---|---|
200 | An object with data |
404 | { "error": … } |
500 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/deployments/claim' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>'POST /api/v1/agent/deployments/result
| Who can call it | Enrolled agent |
| Handler | agentHandler.ReportDeploymentResult |
Request body
DeploymentResult is what the host did with an assignment.
| Field | Type | Description |
|---|---|---|
job_id | string | |
success | boolean | |
detail | string | |
error | string |
Responses
| Status | Body |
|---|---|
200 | An object with data |
400 | { "error": … } · a job id is required |
403 | An object with code (string) · this deployment does not belong to this host |
404 | { "error": … } |
500 | { "error": … } |
503 | { "error": … } · deployments are not running on this core |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/deployments/result' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>' \
-H 'Content-Type: application/json' \
-d '{
"job_id": "<job_id>",
"success": false,
"detail": "<detail>",
"error": "<error>"
}'POST /api/v1/agent/enrol
| Who can call it | Enrolment token |
| Handler | agentHandler.Enrol |
Enrolment is the one agent call that is not signed, because the agent has no identity yet — this is the request that gives it one. It is authenticated by a one-use enrolment token instead.
The enrolment token is sent as token in the request body, not as a header.
Request body
| Field | Type | Description | |
|---|---|---|---|
token | string | required | |
public_key | string | required | PublicKey is the agent's own, generated on its host. The private half is not sent, not asked for, and has no field to arrive in. |
name | string | ||
hostname | string | ||
platform | string | ||
version | string | ||
heartbeat_interval_seconds | integer |
Responses
| Status | Body |
|---|---|
201 | An object with agent_id, heartbeat_interval_seconds, key_id, name, server_time |
400 | { "error": … } |
401 | { "error": … } · this enrolment token is not valid |
409 | { "error": … } · this enrolment token was used up while this request was in flight |
500 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/enrol' \
-H 'Content-Type: application/json' \
-d '{
"token": "<token>",
"public_key": "<public_key>",
"name": "<name>",
"hostname": "<hostname>",
"platform": "<platform>"
}'POST /api/v1/agent/heartbeat
| Who can call it | Enrolled agent |
| Handler | agentHandler.Heartbeat |
Request body
| Field | Type | Description |
|---|---|---|
version | string | |
platform | string | |
hostname | string | |
interval_seconds | integer |
Responses
| Status | Body |
|---|---|
200 | An object with agent_id, server_time |
400 | { "error": … } |
403 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/heartbeat' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>' \
-H 'Content-Type: application/json' \
-d '{
"version": "<version>",
"platform": "<platform>",
"hostname": "<hostname>",
"interval_seconds": 0
}'POST /api/v1/agent/installations
| Who can call it | Enrolled agent |
| Handler | agentHandler.ReportInstallations |
Where the host put them, and what happened when it did. Reported upwards only: there is deliberately no route by which this core can tell a host which files to write or what command to run.
Request body
InstallationReport is the full state of one host's destinations. Full state rather than a delta, exactly like the inventory report, and for the same reason: a lost report costs nothing because the next one carries everything, and the core's picture converges without either side keeping a cursor the other could disagree with.
| Field | Type | Description |
|---|---|---|
reported_at | timestamp | |
spec_path | string | SpecPath is where the host read its destinations from, so "the core shows no destinations for this machine" can be told apart from "the machine is reading a file nobody edited". |
installations | Installation[] | |
errors | string[] | Errors are destinations that could not even be attempted — an unreadable spec, a directory that does not exist. Reported rather than swallowed. |
Responses
| Status | Body |
|---|---|
200 | An object with data, server_time, summary |
400 | { "error": … } |
404 | { "error": … } |
500 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/installations' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>' \
-H 'Content-Type: application/json' \
-d '{
"reported_at": "2026-01-01T00:00:00Z",
"spec_path": "<spec_path>",
"installations": [
"<Installation>"
],
"errors": [
"<errors>"
]
}'POST /api/v1/agent/inventory
| Who can call it | Enrolled agent |
| Handler | agentHandler.Inventory |
What is on the host. Certificates as PEM plus the facts only a process on the machine can see; no field a private key could arrive in.
Request body
InventoryReport is what one scan of one host produced.
| Field | Type | Description |
|---|---|---|
scanned_at | timestamp | |
paths | string[] | |
files_seen | integer | |
certificates | Discovered[] | |
errors | string[] | Errors are directories or files that could not be read. Reported rather than swallowed: "found nothing under /etc/pki" and "could not read /etc/pki" are opposite conclusions and look identical in a result set that omits the second. |
truncated | boolean | Truncated is set when the scan hit one of its own limits, so a short list is never mistaken for a small host. |
Responses
| Status | Body |
|---|---|
200 | An object with data, server_time |
400 | { "error": … } |
404 | { "error": … } |
500 | { "error": … } |
Example request
curl -X POST 'https://certpilot.example.com/api/v1/agent/inventory' \
-H 'X-CertPilot-Agent: <agent id>' \
-H 'X-CertPilot-Timestamp: <unix seconds>' \
-H 'X-CertPilot-Signature: <base64 ed25519 signature>' \
-H 'Content-Type: application/json' \
-d '{
"scanned_at": "2026-01-01T00:00:00Z",
"paths": [
"<paths>"
],
"files_seen": 0,
"certificates": [
"<Discovered>"
],
"errors": [
"<errors>"
]
}'