Custodian
ReferenceHTTP API

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 Conflict with 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."
}

On this page