API reference
Ask ID MANDAT who stands behind a domain and which AI agents act for it. The answer is JSON, optionally with a digital signature (Ed25519). This page describes what the interface does today. Declarations of the operator (“may”, “mayNot”, limits, countries) are not checked or enforced by ID MANDAT. ID MANDAT confirms identity and authority, not creditworthiness or quality.
Endpoints
| Request | Purpose |
|---|---|
GET /api/v1/verify?domain=example.com | Entry and level for a domain. Optional &agent=Name narrows the result to one agent. |
GET /api/v1/verify?signature_agent=https://signer.example.com | Looks up who declared this key directory (Web Bot Auth, header Signature-Agent). The header value itself also works. |
GET /api/v1/keys | Public keys for checking signatures (Ed25519, base64url without padding). Fields: key_id, public_key, status (active / retired). |
Methods: GET, HEAD, OPTIONS. CORS is open (Access-Control-Allow-Origin: *). Base URL: https://idmandat.com.
Response fields
| Field | Meaning |
|---|---|
level | verified, registered, listed_unverified or none. See below. |
level_text | Plain-language explanation of the level. |
entry_state | Only if an earlier entry is no longer valid: revoked, suspended or expired. Then level is none. Do not rely on earlier answers. |
organization, country, city, industry | Company details as listed. |
agents[] | Agents of the company with may, mayNot, optional limits (per transaction / per day, with currency) and validIn (countries). All declared by the operator. |
agent_found | With &agent=: whether an agent of that name exists on the entry. |
mandate_source | Always declared_by_operator. |
registered_at, last_checked | Entry date and last automatic re-check. |
signed, key_id, issued_at, expires_at, signature | Signature data. If signed is false, the answer must not be treated as proof. |
api, version, beta | Name and version of this interface. |
Levels
- verified: domain, imprint and commercial register or VAT ID checked automatically and re-checked regularly.
- registered: credential file and DNS record on the company domain confirmed. Details are the operator’s own declaration.
- listed_unverified: self-listed, details not checked. This also covers entries that carry the “Basic” seal (domain confirmed via DNS); the seal level is shown on the check page of the domain.
- none: no entry. By itself that is not a sign of wrongdoing.
Errors
| Status | error | Cause |
|---|---|---|
| 400 | invalid_domain | domain missing or not a valid domain. |
| 400 | invalid_signature_agent | signature_agent is not a usable address. |
| 405 | method_not_allowed | Anything other than GET, HEAD, OPTIONS. |
Error bodies are JSON with error and message. An unknown domain is not an error: you get status 200 with "registered": false.
Limits, caching and validity
- Rate limits: there is no fixed per-client limit in this version. Please be considerate: the service runs on shared hosting. If you need many lookups, cache them.
- Caching: responses carry
Cache-Control: public, max-age=60. Caching for one minute is fine. - Validity of a signature: a signed answer is valid for 15 minutes from
issued_at(seeexpires_at). After that, ask again. Do not act on an answer pastexpires_at. - Counting: we count requests per day and channel only. No IP address, no domain, no person is stored.
Verifying the signature
- Take the answer without the field
signature. - Sort all object keys recursively in alphabetical order and write the JSON compactly (no spaces; do not escape Unicode or “/”).
- Check the UTF-8 bytes of that text against
signature.valuewith the public key whosekey_idmatches (Ed25519, base64url without padding). - Check that
expires_athas not passed.
Example in Python (needs the package cryptography):
import base64, json
from datetime import datetime, timezone
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
def b64u(s):
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def kanonisch(x):
# Objektschlüssel rekursiv sortieren, kompakt, UTF-8, "/" und Unicode nicht escapen
return json.dumps(x, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def pruefen(antwort, schluessel):
if not antwort.get("signed"):
return False # nicht signiert: kein Nachweis
ablauf = datetime.strptime(antwort["expires_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
if datetime.now(timezone.utc) > ablauf:
return False # abgelaufen: neu abrufen
key = next(k for k in schluessel["keys"] if k["key_id"] == antwort["key_id"])
ohne = {k: v for k, v in antwort.items() if k != "signature"}
try:
Ed25519PublicKey.from_public_bytes(b64u(key["public_key"])).verify(
b64u(antwort["signature"]["value"]), kanonisch(ohne).encode("utf-8"))
return True
except Exception:
return False
Versioning
Every answer names the version of the interface in version (currently 0.3.0-beta). The interface is in beta. Read fields by name and ignore fields you do not know.
Related
FAQ · The standard · MCP server: https://idmandat.com/mcp · A2A card: https://idmandat.com/.well-known/agent-card.json