Local API reference¶
How your own software on the gateway reads its configuration from the
Bootstrap-Agent. This page is a summary; the full contract is
local-api-v1.openapi.yaml in the release archive (contract version 1.6,
interface_version 1).
Transport and access¶
- HTTP/1.1 over a Unix socket,
/run/bootstrap-agent/api.sock. The agent opens no network port. The host name in a request is ignored. - Access is the socket's file permission: mode 0660, group
bootstrap-agent. Membership in that group is the whole grant. Put in it only the processes that need the configuration: it contains the credentials for your target platform, and a call to the read endpoints returns them. - The agent logs the calling user ID on every call that changes something.
The agent answers with one line of JSON; | jq . indents it.
Example output of cproto-v0.2.0-rc.2 (the shape is real, the values are
replaced). Every call with its real answer is on
Local API examples.
{
"interface_version": 1,
"interface_minor_version": 6,
"agent_version": "cproto-v0.2.0-rc.2",
"state": "ready",
"attestation_mode": "tpm",
"profile_version": 42,
"last_checkin": "2026-01-01T12:00:00Z",
"tpm_transient_avail": 3,
"tpm_loaded_sessions": 0,
"tpm_transient_capacity": 3,
"tpm_transient_used": 0,
"identity_cert_state": "not_configured",
"auth_mode": "tpm",
"auth_mode_source": "server",
"activation": "listen",
"profile_ready_marker": "/etc/iot-config/profile-ready"
}
Endpoints¶
| Call | Answers |
|---|---|
GET /v1/status |
Always 200 with the agent's state, see below. It never fails: an unanswered status looks like a crashed agent. |
GET /v1/profile |
The whole current configuration, exactly as the platform rendered it. 404 with NO_CONFIG before there is one. If-None-Match with the ETag gives 304. |
GET /v1/profile/packages |
The list of packages in it, with version and hash. Same 404 and 304 rules. |
GET /v1/profile/packages/{id} |
The content of one package, without an envelope. 404 if the package or the configuration does not exist. |
POST /v1/profile/packages/{id}/ack |
Tells the agent that you applied the configuration. Body: {"profile_version": 42, "applied": true} (reason is free text when applied is false; it is accepted, not stored), see applied: false. 204 when recorded, 400 for a malformed body, 409 when the version is not the current one, 404 for an unknown package. The version is checked first: an unknown package with an old version gets 409. A body without applied counts as applied: false. |
GET /v1/events |
Long poll, see Without polling. |
The profile can contain secrets
GET /v1/profile and GET /v1/profile/packages/{id} return the
configuration unredacted, including the credentials for your target
platform, private key included. Only the agent's user, root and members of
the group bootstrap-agent can open the socket (mode 0660). Treat everything
these calls return as confidential: never write it to a log, a ticket, a
chat or a bug report, and never copy it off the gateway. To show a
configuration to someone, use the package list (/v1/profile/packages),
which carries only ids, versions and hashes. ?pretty=1 does not protect
you: it adds certificate summaries, and still prints the private key in
full (checked on the lab gateway). Safe calls to copy are on
Local API examples; the ones that print secrets are
at the end of that page.
Example of the shape of GET /v1/profile (every value is invented and
deliberately not real; the structure of content depends on your template):
{"profile_version":42,"packages":[{"id":"example-package","version":42,"sha256":"EXAMPLE-NOT-A-REAL-HASH","content":{"example_key":"EXAMPLE-VALUE-NOT-REAL"}}]}
?pretty=1 on the three read calls adds a hex form of each hash
(sha256_hex) and a summary of every certificate (decoded_certs). It does
not indent (use | jq .), and it does not hide anything: a private key is
printed in full, as without it. The answer is re-assembled, so hash or compare
a configuration only in its plain form. sha256 itself is Base64, as the
platform sends it.
Two ways to confirm¶
Your software tells the agent that the configuration is applied in one of two ways. Use the marker file.
| The marker file (recommended) | ack over this API |
|
|---|---|---|
| What your software does | Writes {"state":"connected"} to /run/wlpc/target-confirmed once it is connected to its target, see Confirm the target. |
POST /v1/profile/packages/{id}/ack with {"profile_version": 42, "applied": true}, once for every package. |
| What the agent does | Acknowledges every package that is not yet acknowledged with applied: true. |
Records that one package. |
| What your software needs | The group wlpc. |
The group bootstrap-agent, the package IDs and the current profile_version. |
The file is one write and covers every package of every configuration. Call
ack yourself only if your software applies packages one by one and must
confirm each one separately.
What sends confirmed. Neither way sends it directly. The agent sends
confirmed as soon as every package of the current configuration is
acknowledged with applied: true, whichever way acknowledged the last one.
confirmed sets the device to ACTIVE, and the first confirmed is the
one billed step, once per device: not complete, not check-ins, and a
repeated confirmed never again (see Ready and
confirmed). One agent
process sends it once per configuration. A package set back to false and
acknowledged again does not make that process send it again; after a restart,
the new process sends it once more for the same configuration when a package
becomes acknowledged again, on an early check-in (the last two rows below).
When both are used, measured with the agent of cproto-v0.2.0-rc.2
against a test service (cproto/e2e/confirmpaths.sh), each case on a fresh
device:
| Case | What happened |
|---|---|
| All packages acknowledged over the API, no file | confirmed was sent once. A file written afterwards changed nothing: no second confirmation. |
| Two of five packages over the API, then the file | No confirmation after the two. The file acknowledged the other three, and confirmed was sent once. |
Four with applied: true, one with applied: false, then the file |
No confirmation, and no request at all went to the platform. The file then overwrote the false with true, and confirmed was sent once. |
applied: false after the confirmation, the file still there, then a restart |
Nothing was sent for the false. At the start the agent read the file, acknowledged the package with true again, and sent the confirmation once more on an early check-in. |
One package set to false and back to true over the API, by the process that had confirmed |
No second confirmation. |
| The same after a restart | The confirmation was sent once more. |
On the lab gateway the file acknowledged all seven packages, and in an agent started on a stored configuration, a package acknowledged again over the API led to an early check-in that carried the confirmation.
An ack for an older profile_version is refused with 409, so software that
is still applying an old configuration cannot mark the new one as delivered.
Acks are idempotent.
applied: false¶
- On the device: the package counts as not acknowledged (
"acked": falsein the package list), and as long as one package is not acknowledged, noconfirmedis sent. The value is stored and survives a restart. Areasonis accepted but neither stored nor sent. - At the platform: nothing. The agent sends no request for an ack, with
trueor withfalse; it only sendsconfirmedonce all aretrue. The platform cannot tell a configuration that failed from one that is still being applied: it only sees thatconfirmeddoes not come. This is a known gap, see Known gaps and limits. - The file wins. Whenever the agent reads a
connectedfile (when it is written, after a new configuration, and at every start), it acknowledges every package that is not acknowledged withapplied: true, including one your software set tofalse. Software that cannot apply a configuration must not write the file, and must remove it if it is there. - Leaving out
appliedcounts asfalse. On the lab gateway an ack with onlyprofile_versionwas answered204and the package showed as not acknowledged.
State¶
state in GET /v1/status is one of:
state |
Meaning |
|---|---|
no_config |
There is no configuration (yet). This is a normal state, not an error, and it can last for months: a device on a shelf that nobody has claimed stays here. It is also the state after the platform declined the device (not known, not claimed, locked); the reason is in last_error_code. Your software must tolerate it indefinitely, without crashing, blocking its start or raising alarms. |
bootstrapping |
A first setup is running. |
ready |
A configuration is stored and served. |
error |
A local fault: a broken TPM, storage that cannot be written, a missing platform address. A verdict of the platform never puts the agent here. |
Other fields: profile_version (an opaque value, compare it only with ==
and !=; a higher number does not mean newer), last_checkin,
last_error_code (left out while there is none), tpm_transient_avail and
tpm_transient_capacity (free and total transient TPM slots),
tpm_transient_used, tpm_loaded_sessions, identity_cert_state,
auth_mode, auth_mode_source, activation (always listen in this agent)
and profile_ready_marker (the path of the marker file). Test for a newer
field with interface_minor_version.
last_error_code carries the platform's public error code unchanged, or one
of the agent's own TPM fault codes, for example
TPM_STUCK_POWER_CYCLE_REQUIRED, see TPM slots.
A refusal that the platform sends during a later check-in sets it too, and a
device that holds a configuration stays ready. The console does not show
such a refused contact, see Known gaps and limits; read
it here or in the agent's log.
Errors¶
An error answer is {"code": "..."} with one of NO_CONFIG, NOT_FOUND,
MALFORMED_REQUEST, STALE_PROFILE_VERSION or INTERNAL. An unknown path
gets 404 with NOT_FOUND. So does a method that a path does not take (for
example POST or DELETE on /v1/status, GET on .../ack): 404, not
405.
For what to do when
a device stays in no_config, see
Troubleshooting.
Without polling¶
You do not need to ask GET /v1/status every second or two to learn about a
new configuration. Two ways instead:
- Long poll.
GET /v1/events?profile_version=PROFILE_VERSIONwaits until the version changes (answer200with the newprofile_version) or 60 seconds pass (204). If the version you pass is already out of date, it answers at once, so a change between two polls is not missed. A closed keep-alive connection is normal; connect again. - A marker file. After every newly stored configuration, and only after it
is safely written, the agent replaces the file
/etc/iot-config/profile-ready. A systemd path unit reacts to that, see Start your software and confirm. An unchanged check-in does not touch the file.