Authenticate a headless agent

The paid console’s API at https://api.muniment.ai authenticates an organization’s agents. Agent access is available at launch. The local app needs no API and no key.

What you need first

Later steps use an organization owner bearer, an Ed25519 key pair, and values from the discovery document. Any visitor can run the discovery request today. At launch, an organization owner issues the bearer. Public visitors cannot enroll agents today.

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, audience, supported profiles, signing algorithm, and lifetime limits.

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

RFC 9728 standardizes OAuth protected-resource metadata. The console’s API publishes the muniment discovery document named above. Use its path and returned values exactly. Do not derive 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.

create the agent principal
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.

issue the enrollment
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. Follow these steps:

  1. Build the AgentEnrollmentConsumeRequest in the consume the enrollment block.
  2. Replace the illustrative NumericDates with current integer Unix seconds.
  3. For both proof types, choose a fresh unique jti.
  4. Require iat no more than 60 seconds ahead.
  5. Require nbf no more than 60 seconds ahead.
  6. Require exp no more than 60 seconds behind. Also require exp > nbf.
  7. Require exp-iat <= 300. These boundaries are inclusive.
  8. Compute SECRET as unpadded base64url of SHA-256 over the ASCII enrollment secret.
  9. Join the enrollment signing transcript lines with LF, with no final LF.
  10. Sign those exact UTF-8 bytes with the submitted Ed25519 private key.
  11. Put the unpadded base64url signature in key_proof.signature.
enrollment 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>
consume the enrollment
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

Follow these exchange signing steps:

  1. Build the KeyExchangeRequest in the exchange for a bearer block. Its signed_body contains exactly grant_type,requested_token_type, and the discovered audience.
  2. Replace the illustrative NumericDates with current integer Unix seconds.
  3. Canonicalize that object with RFC 8785 JCS.
  4. Hash its UTF-8 bytes with SHA-256.
  5. Set BODY to the digest’s unpadded base64url encoding.
  6. Join the key exchange signing transcript lines with LF, with no final LF.
  7. Sign those exact UTF-8 bytes with the enrolled Ed25519 key.
  8. Place the unpadded base64url signature in key_proof.signature.
key exchange 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>
exchange for a bearer
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

Federated profiles use the alternative federated request published by the contract. Use the iat, nbf, exp, and exp-iat proof time boundaries.

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 console API

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

call the console API
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.

An agent never exceeds the human it acts for.

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.

revoke 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
revoke one agent
curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
  https://api.muniment.ai/v1/agents/<agent-id>/revoke
advance the organization epoch
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 paid console’s API. It does not claim public availability for a Muniment CLI, editor extension, Desktop app, Mobile app, or admin UI.

Next steps

After your first console API call, review the controls, costs, and service health endpoint.

Search docs

Join the waitlist

Get desktop release updates.

We will email you about desktop releases and new features. muniment is a desktop workspace for your models, tools, and files.