Skip to content

The agent API

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

MethodPathWho can call it
POST/api/v1/agent/certificatesEnrolled agent
POST/api/v1/agent/deployments/claimEnrolled agent
POST/api/v1/agent/deployments/resultEnrolled agent
POST/api/v1/agent/enrolEnrolment token
POST/api/v1/agent/heartbeatEnrolled agent
POST/api/v1/agent/installationsEnrolled agent
POST/api/v1/agent/inventoryEnrolled agent

POST /api/v1/agent/certificates

Who can call itEnrolled agent
HandleragentHandler.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.

FieldTypeDescription
csr_pemstringCSRPem carries the names, the public key, and a signature proving the requester holds the private half. Nothing else in it is honoured.
install_pathstringInstallPath is where the agent intends to keep it, recorded so the inventory can be matched against issuance without guessing.
renewsstringRenews is the certificate this replaces, when the agent is rotating a key it already holds.

Responses

StatusBody
201An object with data
400{ "error": … }
403An object with code (string)
404{ "error": … }
Example request
bash
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 itEnrolled agent
HandleragentHandler.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

StatusBody
200An object with data
404{ "error": … }
500{ "error": … }
Example request
bash
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 itEnrolled agent
HandleragentHandler.ReportDeploymentResult

Request body

DeploymentResult is what the host did with an assignment.

FieldTypeDescription
job_idstring
successboolean
detailstring
errorstring

Responses

StatusBody
200An object with data
400{ "error": … }
· a job id is required
403An 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
bash
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 itEnrolment token
HandleragentHandler.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

FieldTypeDescription
tokenstringrequired
public_keystringrequiredPublicKey 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.
namestring
hostnamestring
platformstring
versionstring
heartbeat_interval_secondsinteger

Responses

StatusBody
201An 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
bash
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 itEnrolled agent
HandleragentHandler.Heartbeat

Request body

FieldTypeDescription
versionstring
platformstring
hostnamestring
interval_secondsinteger

Responses

StatusBody
200An object with agent_id, server_time
400{ "error": … }
403{ "error": … }
Example request
bash
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 itEnrolled agent
HandleragentHandler.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.

FieldTypeDescription
reported_attimestamp
spec_pathstringSpecPath 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".
installationsInstallation[]
errorsstring[]Errors are destinations that could not even be attempted — an unreadable spec, a directory that does not exist. Reported rather than swallowed.

Responses

StatusBody
200An object with data, server_time, summary
400{ "error": … }
404{ "error": … }
500{ "error": … }
Example request
bash
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 itEnrolled agent
HandleragentHandler.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.

FieldTypeDescription
scanned_attimestamp
pathsstring[]
files_seeninteger
certificatesDiscovered[]
errorsstring[]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.
truncatedbooleanTruncated is set when the scan hit one of its own limits, so a short list is never mistaken for a small host.

Responses

StatusBody
200An object with data, server_time
400{ "error": … }
404{ "error": … }
500{ "error": … }
Example request
bash
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>"
  ]
}'