Principals
Mint, list and revoke the identities in a vault over HTTP, and read the refusals that guard them.
A principal is one identity in one vault. Its kind decides what it can reach:
an operator administers the vault, a service runs an unattended machine, and an
agent reaches only the /mcp endpoint. Creating and revoking
a principal are reserved for operator principals. Listing is not: any principal
except an agent may list the vault's principals, so a service can read them too.
No agent reaches any of these routes.
A service calling create or revoke is refused:
{
"code": "caller_not_operator",
"message": "Creating a principal is reserved for operator principals; this credential is a service.",
"details": { "kind": "service" }
}POST /v1/principals — mint a principal
POST /v1/principals
Authorization: Bearer <operator token>
Content-Type: application/json
{
"principal": {
"slug": "liberty-agent",
"kind": "service"
}
}slug must be lowercase letters, digits and hyphens. kind must be one of agent,
service or operator. On success the response is 201 Created and carries the
token once:
{
"principal": {
"slug": "liberty-agent",
"kind": "service",
"created_at": "2026-08-12T09:05:00Z",
"revoked": false
},
"token": "<the principal's token>"
}No later route returns the token — the list route below never returns a token, a digest or a selector. Capture it now.
The refusals:
-
a slug outside the permitted characters is
invalid_principal_slug(422):{ "code": "invalid_principal_slug", "message": "A principal slug must be lowercase letters, digits and hyphens; \"Liberty Agent\" is not.", "details": { "slug": "Liberty Agent" } } -
a kind outside the three is
invalid_principal_kind(422):{ "code": "invalid_principal_kind", "message": "A principal kind must be one of agent, service or operator; \"robot\" is not.", "details": { "kind": "robot" } } -
a slug already in use in this vault is a bare
409 Conflictwith no body.
GET /v1/principals — list the vault's principals
Lists every principal in the vault, revoked ones included, each with a count of its
live grants and the time of its most recent read. It never returns a token. Any
principal except an agent may call it, so a service can list the vault's
principals as well as an operator.
GET /v1/principals
Authorization: Bearer <operator or service token>{
"principals": [
{
"slug": "operator",
"kind": "operator",
"created_at": "2026-08-12T09:00:00Z",
"revoked_at": null,
"live_grant_count": 0,
"last_read_at": null,
"last_read_is_lower_bound": true
},
{
"slug": "liberty-agent",
"kind": "service",
"created_at": "2026-08-12T09:05:00Z",
"revoked_at": null,
"live_grant_count": 3,
"last_read_at": "2026-08-27T14:11:07Z",
"last_read_is_lower_bound": true
}
]
}last_read_at is a lower bound, not an exact answer: token and masked reads are
sampled, so the true last read may be more recent. last_read_is_lower_bound is
always true, so a client does not mistake it for exact.
DELETE /v1/principals/:slug — revoke a principal
Stamps the principal revoked_at and leaves the row, so the audit trail's
references to it still resolve. Its token stops authenticating immediately.
DELETE /v1/principals/liberty-agent
Authorization: Bearer <operator token>{
"principal": {
"slug": "liberty-agent",
"kind": "service",
"revoked_at": "2026-08-28T10:00:00Z"
}
}Revocation is idempotent: a second call reports the original revoked_at and writes
nothing further. An unknown slug takes the shared not_found.
You cannot revoke a vault's last live operator — doing so would leave the vault with no principal able to administer it:
{
"code": "last_operator_irremovable",
"message": "This is the vault's last live operator principal; revoking it would leave the vault with no principal able to administer principals or grants. Wind a vault down with `custodian key destroy` instead."
}