API / Authentication

# Authenticate a headless agent

The agent authentication API is live at `https://api.muniment.ai`. Discover its contract, enroll an organization-owned agent, exchange a signed proof for a short-lived bearer, and revoke that authority when it is no longer needed.

## Discover the contract

Fetch the live `muniment.agent-auth/1` discovery document before enrolling or exchanging a token. It supplies the issuer, enrollment endpoint, token endpoint, gateway audience, supported profiles, signing algorithm, and lifetime limits.

shell

```
curl --fail-with-body \
  https://api.muniment.ai/.well-known/muniment-agent-auth
```

[RFC 9728](https://www.rfc-editor.org/rfc/rfc9728.html) standardizes OAuth protected-resource metadata. This gateway publishes the Muniment discovery document named above; use its path and returned values exactly rather than deriving a different well-known path or schema.

## Enroll an agent

A freshly authenticated organization owner first creates the agent principal. Keep the returned agent ID and owner approval ID for the enrollment request.

shell

```
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  --header "Content-Type: application/json" \
  --data '{
    "display_name": "build-agent",
    "owner_user_id": "<owner-user-uuid>"
  }' \
  https://api.muniment.ai/v1/agents
```

Generate an Ed25519 key pair in the agent's credential store, then submit its public JWK. Choose fresh, opaque profile and key identifiers that match the discovery contract.

shell

```
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  --header "Content-Type: application/json" \
  --data '{
    "contract_version": "muniment.agent-auth/1",
    "agent_id": "<agent-id>",
    "profile_id": "<new-profile-id>",
    "profile_type": "agent_key",
    "public_key": {
      "kty": "OKP",
      "crv": "Ed25519",
      "kid": "<key-id>",
      "x": "<43-character-base64url-public-key>"
    },
    "owner_approval_id": "<owner-approval-id>"
  }' \
  https://api.muniment.ai/v1/auth/agent/enrollments
```

The response displays the enrollment secret once. Build the `AgentEnrollmentConsumeRequest` below and replace the illustrative NumericDates with current integer Unix seconds. Compute `SECRET` as unpadded base64url of SHA-256 over the ASCII enrollment secret. Join the following lines with LF, with no final LF, sign those exact UTF-8 bytes with the submitted Ed25519 private key, and put the unpadded base64url signature in `key_proof.signature`. For both proof types, choose a fresh unique `jti`; require `iat` and `nbf` no more than 60 seconds ahead, `exp` no more than 60 seconds behind, `exp > nbf`, and `exp-iat <= 300`. These boundaries are inclusive.

exact signing transcript

```
MUNIMENT-AGENT-ENROLLMENT-V1
POST
/v1/auth/agent/enrollments/consume
<enrollment-id>
<SECRET>
<profile-id>
<key-id>
<iat-decimal>
<nbf-decimal>
<exp-decimal>
<fresh-unique-jti>
```

shell

```
curl --fail-with-body \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{
    "contract_version": "muniment.agent-auth/1",
    "enrollment_id": "<enrollment-id>",
    "enrollment_secret": "<display-once-enrollment-secret>",
    "key_proof": {
      "alg": "EdDSA",
      "profile_id": "<profile-id>",
      "key_id": "<key-id>",
      "iat": 1700000000,
      "nbf": 1700000000,
      "exp": 1700000300,
      "jti": "<fresh-unique-jti>",
      "signature": "<unpadded-base64url-ed25519-signature>"
    }
  }' \
  https://api.muniment.ai/v1/auth/agent/enrollments/consume
```

Exactly one concurrent consumer succeeds. An expired, revoked, replayed, or already consumed enrollment fails closed.

## Exchange for a bearer

Build the `KeyExchangeRequest` below. Its `signed_body` contains exactly `grant_type`,`requested_token_type`, and the discovered `audience`. Canonicalize that object with RFC 8785 JCS, hash its UTF-8 bytes with SHA-256, and set `BODY` to the digest's unpadded base64url encoding. Join the following lines with LF, with no final LF, sign those exact UTF-8 bytes with the enrolled Ed25519 key, and place the unpadded base64url signature in `key_proof.signature`. Replace the illustrative NumericDates with current integer Unix seconds. Federated profiles use the alternative federated request published by the contract.

exact signing transcript

```
MUNIMENT-AGENT-KEY-V1
POST
/v1/auth/agent/token
<BODY>
<profile-id>
<key-id>
<discovered-gateway-audience>
<iat-decimal>
<nbf-decimal>
<exp-decimal>
<fresh-unique-jti>
```

shell

```
curl --fail-with-body \
  --request POST \
  --header "Content-Type: application/json" \
  --data '{
    "contract_version": "muniment.agent-auth/1",
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "subject_token_type": "urn:muniment:params:oauth:token-type:agent-key-assertion",
    "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
    "audience": "<discovered-gateway-audience>",
    "signed_body": {
      "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
      "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
      "audience": "<discovered-gateway-audience>"
    },
    "key_proof": {
      "alg": "EdDSA",
      "profile_id": "<profile-id>",
      "key_id": "<key-id>",
      "iat": 1700000000,
      "nbf": 1700000000,
      "exp": 1700000300,
      "jti": "<fresh-unique-jti>",
      "signature": "<unpadded-base64url-ed25519-signature>"
    }
  }' \
  https://api.muniment.ai/v1/auth/agent/token
```

The returned EdDSA bearer has only `model:invoke` scope and a maximum lifetime of 600 seconds. There is no refresh token. Exchange another fresh, replay-safe signed proof when the bearer expires.

## Call the gateway

Send the short-lived bearer in the HTTP authorization header. Never put it in a URL or request body.

shell

```
curl --fail-with-body \
  --header "Authorization: Bearer ${MUNIMENT_AGENT_BEARER}" \
  --header "Content-Type: application/json" \
  --data '{"model":"<entitled-model>","messages":[{"role":"user","content":"Reply with one word."}]}' \
  https://api.muniment.ai/v1/chat/completions
```

Admission checks the current organization, agent principal, profile, and revocation epoch. For verified CrewAI and PydanticAI setup, and for the three supported wire protocols, continue to [model-tool connection recipes](/docs/connections/).

## Receipt attribution

An admitted call produces tenant-scoped receipt provenance attributed from the validated server-side agent identity. Caller-supplied organization, user, role, grant, entitlement, budget, or provider-key values cannot establish authority. This contract does not publish receipt response fields or a receipt-read endpoint.

## Revoke access

A freshly authenticated organization owner can revoke one profile, revoke an agent and all of its credentials, or advance the organization epoch to invalidate all existing agent sessions.

one profile

```
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  https://api.muniment.ai/v1/agents/<agent-id>/profiles/<profile-id>/revoke
```

one agent

```
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  https://api.muniment.ai/v1/agents/<agent-id>/revoke
```

organization emergency control

```
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  https://api.muniment.ai/v1/agents/revocations/epoch
```

Revocation is checked against live authority; a bearer with a stale profile, principal, or organization epoch is denied.

## Credential safety

Treat the owner bearer, enrollment secret, private key, signed assertion, and agent bearer as identity credentials. Keep them out of source control, command history, support messages, fixtures, and application logs. Store the private key in the agent's credential store, redact authorization headers, and never substitute an OpenAI or other provider key. Provider credentials remain server-side.

This reference documents the live gateway API only. It does not claim public availability for a Muniment CLI, editor extension, Desktop app, Mobile app, or admin UI.
