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.
curl --fail-with-body \
https://api.muniment.ai/.well-known/muniment-agent-authRFC 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.
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/agentsGenerate 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.
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/enrollmentsThe response displays the enrollment secret once. Follow these steps:
- Build the
AgentEnrollmentConsumeRequestin theconsume the enrollmentblock. - Replace the illustrative NumericDates with current integer Unix seconds.
- For both proof types, choose a fresh unique
jti. - Require
iatno more than 60 seconds ahead. - Require
nbfno more than 60 seconds ahead. - Require
expno more than 60 seconds behind. Also requireexp > nbf. - Require
exp-iat <= 300. These boundaries are inclusive. - Compute
SECRETas unpadded base64url of SHA-256 over the ASCII enrollment secret. - Join the
enrollment signing transcriptlines with LF, with no final LF. - Sign those exact UTF-8 bytes with the submitted Ed25519 private key.
- Put the unpadded base64url signature in
key_proof.signature.
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>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/consumeExactly one concurrent consumer succeeds. An expired, revoked, replayed, or already consumed enrollment fails closed.
Exchange for a bearer
Follow these exchange signing steps:
- Build the
KeyExchangeRequestin theexchange for a bearerblock. Itssigned_bodycontains exactlygrant_type,requested_token_type, and the discoveredaudience. - Replace the illustrative NumericDates with current integer Unix seconds.
- Canonicalize that object with RFC 8785 JCS.
- Hash its UTF-8 bytes with SHA-256.
- Set
BODYto the digest’s unpadded base64url encoding. - Join the
key exchange signing transcriptlines with LF, with no final LF. - Sign those exact UTF-8 bytes with the enrolled Ed25519 key.
- Place the unpadded base64url signature in
key_proof.signature.
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>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/tokenFederated 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.
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/completionsAdmission 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.
curl --fail-with-body \
--request POST \
--header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
https://api.muniment.ai/v1/agents/<agent-id>/profiles/<profile-id>/revokecurl --fail-with-body \
--request POST \
--header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
https://api.muniment.ai/v1/agents/<agent-id>/revokecurl --fail-with-body \
--request POST \
--header "Authorization: Bearer ${MUNIMENT_OWNER_BEARER}" \
https://api.muniment.ai/v1/agents/revocations/epochRevocation 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.