Documentation
Quickstart
One call decides, and returns the receipt. Everything else is administration.
1. Store a policy
Policies are YAML and immutable per version: posting the same name again creates version 2, and the version that applied stays readable for every receipt that referenced it.
cat > policy.yaml <<'YAML'
policy: default
rules:
- id: purchase
when:
intent: purchase
require:
agent_verified: true
max_amount: "500.00 EUR"
decision: allow
otherwise: review
default: deny
YAML
curl -X POST https://api.mandaton.eu/v1/policies \
-H "Authorization: Bearer $MANDATON_KEY" \
-H "Content-Type: text/plain" \
--data-binary @policy.yaml
JSON works too, with the YAML in a source field — which is what the
dashboard's policy editor posts.
2. Ask for a decision
Pass the incoming request as you received it — method, URL and headers. The signature headers are what get verified, so they must arrive unmodified.
curl -X POST https://api.mandaton.eu/v1/verify \
-H "Authorization: Bearer $MANDATON_KEY" \
-H "Content-Type: application/json" \
-d '{
"request": {
"method": "POST",
"url": "https://shop.example.eu/api/orders",
"headers": {
"signature": "sig1=:…:",
"signature-input": "sig1=(\"@method\" \"@authority\" \"@path\");created=…;keyid=\"…\";alg=\"ed25519\";tag=\"web-bot-auth\"",
"signature-agent": "\"https://agents.example\""
}
},
"policy": "default",
"context": { "intent": "purchase", "amount": "129.00 EUR", "resource": "sku:1234" }
}'
{
"decision": "allow",
"agent": { "verified": true, "method": "web-bot-auth",
"key_id": "poqkLGiUqyqJZmZ…", "operator": "agents.example",
"alg": "ed25519" },
"policy": { "name": "default", "version": 1, "matched_rule": "purchase" },
"reasons": ["agent signature verified", "amount within per-transaction limit"],
"receipt": { "id": "rcpt_…", "hash": "9f2b…", "seal_status": "pending",
"log_index": 41, "verify_url": "https://r.mandaton.eu/r/rcpt_…" },
"latency_ms": 8
}
Act on decision: serve the request, route it to a human, or refuse. Keep
receipt.verify_url — that link is what a counterparty checks later.
3. Register the agent keys you trust
curl -X POST https://api.mandaton.eu/v1/agents \
-H "Authorization: Bearer $MANDATON_KEY" \
-H "Content-Type: application/json" \
-d '{"key_id": "poqkLGiUqyqJZmZ…", "operator": "agents.example",
"jwk": { "kty": "OKP", "crv": "Ed25519", "x": "…" }}'
The key_id is the JWK thumbprint (RFC 8037/8785 style), which is what the
signing agent puts in signature-input.
4. Verify a receipt without an account
GET /r/:id is public and unauthenticated: a receipt only its issuer can
check is not evidence to anybody else. In a browser it renders as a page; with
Accept: application/json or ?format=json it returns the
proof material.
curl https://r.mandaton.eu/r/rcpt_… | jq '{hash_matches, log}'
You get the receipt body, the hash we stored, the hash recomputed from the body, the inclusion path into the Merkle tree, and the signed tree head. Recompute all three yourself: canonicalise the body per RFC 8785, SHA-256 it, and walk the inclusion path per RFC 6962.
Routes
| Route | Auth | Purpose |
|---|---|---|
| POST /v1/verify | bearer | the decision; returns receipt and verify_url |
| GET /v1/receipts | bearer | your own receipts |
| GET /v1/receipts/:id | bearer | one receipt with its body |
| GET /r/:id | none | public verification: hash, inclusion proof, tree head |
| GET /v1/log/head | none | current signed tree head |
| POST /v1/policies | bearer | store a policy version |
| GET /v1/policies | bearer | list policies |
| POST /v1/agents | bearer | register an agent key |
| GET /health | none | liveness |
Latency and failure behaviour
Median 8 ms in production, against a p99 budget of 50 ms. Sealing and tree-head signing happen off the request path. If the decision service is unreachable, the call that fails is ours — decide in your own code whether that means fail-open or fail-closed, and write that choice down before you need it.