Agent trust APIs
These endpoints help an application or gateway decide whether to let an AI agent use a tool. You can inspect what an MCP server or A2A agent publishes, verify a chain of signed delegations (“the bank allowed this agent to refund payments”), and ask whether that chain allows one specific tool call.
DID.is checks signatures and records what servers publish. It does not observe what an agent actually does. A valid chain proves who delegated what to whom; it does not prove the agent stays within that scope.
- Each receipt signature and its issuer DID
- Links: each step narrows the previous one
- Dates and trusted root
- The leaf capabilities grant this tool
What is checked
Section titled “What is checked”- Delegation receipts. Each Delegation Receipt v1 JWS has a valid structure and is signed by a key of its issuer DID. The chain runs root first, each link points to the previous one (hash linkage), each step grants only capabilities the previous one had (attenuation), there are no cycles, every receipt is within its validity window, and the chain is at most 8 long.
- Trusted root. When you pass
trustedRoots, the root of the chain must be one of them. - Tool grant. A capability grants a tool when it is
*, the exact tool name (payments:transfergrants onlypayments:transfer), orns:*for a tool namedns:…(payments:*never grantsrefundorslack:post). - MCP servers. DID.is connects to the server, lists its tools and hashes each tool definition and schema, so you can see when they change.
- A2A agent cards. The card is fetched from
/.well-known/agent-card.json, validated against A2A 1.0, and anysignatures[]are verified.
What is not checked
Section titled “What is not checked”- Agent behaviour. Nothing here observes or limits what the agent does after a call is allowed.
- Server identity. MCP
serverInfois self-reported; MCP has no server identity attestation. - Tool safety.
heuristicClassis a DID.is estimate from names, descriptions and schemas;annotationsare the server’s own claims. - Standard status. Delegation receipts are a DID.is experimental format, not a ratified standard.
Choose an endpoint
Section titled “Choose an endpoint”| Endpoint | Question it answers | Budget |
|---|---|---|
POST /v1/agents/authorize |
Does this chain allow this tool? (ALLOW / DENY) | Intensive |
POST /v1/agents/verify-delegation |
Is this delegation chain valid, and what can the leaf do? | Intensive |
GET /v1/agents/verify-tool?agent=&tool=&root= |
Does a registered chain allow this agent to use this tool? | Standard |
GET /v1/mcp/inspect?endpoint= |
Which tools does this MCP server expose, and have they changed? | Intensive |
GET /v1/a2a/inspect?url= |
Is this A2A agent card well formed, and who signed it? | Intensive |
POST /v1/fast-verify/jws |
Was this JWS signed by a key of the DID in its kid? |
Standard |
The intensive budget is 10 requests per minute, the standard budget 20. See Errors and rate limits.
Authorise a tool call
Section titled “Authorise a tool call”POST /v1/agents/authorize verifies the chain and checks one tool against the leaf’s capabilities. It is stateless: nothing is stored.
curl -X POST https://did.is/api/v1/agents/authorize \ -H 'content-type: application/json' \ -d '{"chain":["<root receipt>","<leaf receipt>"],"tool":"payments.refund","trustedRoots":["did:web:bank.example"]}'import { DidisClient } from "@didis/client";
const didis = new DidisClient();const { authorization } = await didis.authorize(chain, "payments.refund", { trustedRoots: ["did:web:bank.example"],});if (authorization.decision !== "ALLOW") throw new Error(authorization.reason);from didis import DidisClient
with DidisClient() as didis: res = didis.authorize(chain, "payments.refund", trusted_roots=["did:web:bank.example"]) if res["authorization"]["decision"] != "ALLOW": raise PermissionError(res["authorization"]["reason"])Replace the placeholders with real receipt JWS strings. The response has an authorization object (agent, tool, decision, authorized, reason, and when a capability matched, matchedCapability, root and expires) and the full chain result.
Reading the result
Section titled “Reading the result”| Result | What it means | What to do |
|---|---|---|
ALLOW |
The chain is VALID and a leaf capability grants the tool. reason names it, for example capability 'payments:*' grants 'refund'. |
Let the call through. |
DENY, scope violation |
The chain is valid, but no capability grants this tool. | Refuse the call. |
DENY, chain not valid |
The chain is INVALID, INDETERMINATE or MALFORMED; reason is the chain headline. |
Refuse the call. |
DENY, nothing registered |
verify-tool only: no VALID chain is registered for this agent. |
Register a chain, or use authorize. |
Chain status values from verify-delegation:
Chain status |
What it means |
|---|---|
VALID |
Every link verified; effectiveCapabilities and notAfter describe what the leaf may do and until when. |
INVALID |
A link failed, or the root is not in trustedRoots. The chain grants nothing. |
INDETERMINATE |
A link could not be fully verified, for example an issuer DID did not resolve. Treat it as unauthorised. |
MALFORMED |
Not a usable chain, for example empty or longer than 8 receipts. |
Treat any error response the same as DENY.
Verify a delegation chain
Section titled “Verify a delegation chain”Send receipts root first.
curl -X POST https://did.is/api/v1/agents/verify-delegation \ -H 'content-type: application/json' \ -d '{"chain":["<root receipt>","<leaf receipt>"],"trustedRoots":["did:web:bank.example"],"register":false}'The response has registered and a result with status, headline, checks[], links[], root, leaf, effectiveCapabilities, notAfter, trustedRoot and limitations. With register: true, a VALID chain is stored for verify-tool. SDK: verifyDelegation(chain, { trustedRoots, register }) / verify_delegation(chain, trusted_roots=…, register=…).
An empty chain shows the shape of a failure (live response):
{ "registered": false, "result": { "status": "MALFORMED", "headline": "A chain must contain between 1 and 8 receipts.", "checks": [ { "id": "depth", "label": "Chain depth", "status": "FAIL", "detail": "A chain must contain between 1 and 8 receipts." } ], "effectiveCapabilities": [], "format": "DID.is Delegation Receipt v1 (experimental)", "leaf": null, "links": [], "notAfter": null, "root": null, "trustedRoot": null, "limitations": [ "Delegation receipts are a DID.is experimental format, not a ratified standard.", "A valid chain proves who delegated what to whom; it does not prove the delegatee behaves within scope." ] }}Check a registered agent
Section titled “Check a registered agent”After registering a chain with register: true, ask about an agent without sending the chain again:
curl 'https://did.is/api/v1/agents/verify-tool?agent=did:web:agent.example&tool=payments:transfer'const auth = await didis.verifyTool("did:web:agent.example", "payments:transfer");console.log(auth.decision, auth.reason);auth = didis.verify_tool("did:web:agent.example", "payments:transfer")print(auth["decision"], auth["reason"]){ "agent": "did:web:agent.example", "tool": "payments:transfer", "decision": "DENY", "authorized": false, "reason": "no verified delegation is registered for this agent"}Add root= to require a specific delegation root. Without a registered VALID chain the answer is always DENY.
Inspect an MCP server
Section titled “Inspect an MCP server”curl 'https://did.is/api/v1/mcp/inspect?endpoint=https://mcp.deepwiki.com/mcp'const mcp = await didis.inspectMcp("https://mcp.deepwiki.com/mcp");for (const t of mcp.tools) console.log(t.name, t.heuristicClass, t.drift);mcp = didis.inspect_mcp("https://mcp.deepwiki.com/mcp")for t in mcp["tools"]: print(t["name"], t["heuristicClass"], t["drift"])Abridged live response:
{ "endpoint": "https://mcp.deepwiki.com/mcp", "status": "INSPECTED", "mode": "SESSION", "negotiatedVersion": "2025-11-25", "serverInfo": { "name": "DeepWiki", "version": "2.14.3" }, "headline": "DeepWiki — MCP 2025-11-25 (legacy session); 3 tools, 0 with risk signals; definitions unchanged.", "tools": [ { "name": "ask_wiki_question", "definitionSha256": "d9a292d4a04af2afd0545f2a723be7e0d73fe15287ce788870f1ccd71c36230b", "schemaSha256": "fc67cfc7a6567b5e0626916674d71a6cea7adcd20e88636705dc5b577846f2e3", "annotations": null, "heuristicClass": "read_only", "riskSignals": [], "drift": "UNCHANGED" } ], "drift": { "status": "UNCHANGED", "added": [], "removed": [], "changed": [] }}Store definitionSha256 and schemaSha256 for the tools you approve. If they change later, review the tool again. A tool’s drift is FIRST_OBSERVATION, UNCHANGED, CHANGED or ADDED, measured against DID.is’s previous observation of the same endpoint. A server that requires OAuth returns AUTH_REQUIRED and its tools are not listed.
The response also has checks[], inventoryHash, classCounts, tls and limitations. Each check is PASS, WARN or SKIP. WARN is a caution to read, not a failure: the inspection still succeeded.
| Check | PASS |
WARN |
SKIP |
|---|---|---|---|
historyPersistence |
This inventory was stored for later drift comparison. | It was not stored; the next comparison may use an older snapshot. | |
transport |
TLS to the server was validated. | No TLS. | |
protocol |
The server answered stateless MCP discovery. | The server only supports the older session handshake. | |
identity |
Always: serverInfo is self-reported and not authenticated. |
||
inventory |
No tool has risk signals. | At least one tool has risk signals; review those tools. | |
drift |
Definitions are identical to the previous observation. | Tools were added, removed or changed since then; review them. | First observation, or the hashing profile changed. |
Inspect an A2A agent card
Section titled “Inspect an A2A agent card”curl 'https://did.is/api/v1/a2a/inspect?url=https://agent.example.com'const card = await didis.inspectA2a("https://agent.example.com");console.log(card.headline, card.signatures.map((s) => s.status));card = didis.inspect_a2a("https://agent.example.com")print(card["headline"], [s["status"] for s in card["signatures"]])DID.is fetches /.well-known/agent-card.json (with a fallback to the legacy agent.json), validates it against A2A 1.0 and verifies any signatures[] (JWS over the JCS-canonicalized card) with keys from a DID kid or an HTTPS jku. The response has specVersion (1.0, 0.3 or unknown), headline, signatures[] (each VALID, INVALID, UNSUPPORTED or INDETERMINATE) and a checks[] list (fetch, schema, version, interfaces, origin, provider, security, signature).
If the URL has no agent card, the request fails with a problem instead of a result. For example, url=https://did.is currently returns 422 INVALID_DID_DOCUMENT because https://did.is/.well-known/agent-card.json is not JSON. See Errors and rate limits.
Verify a signed JWS
Section titled “Verify a signed JWS”POST /v1/fast-verify/jws takes a compact JWS whose kid is a DID URL, resolves the DID and verifies the signature.
{ "jws": "<compact JWS>", "relationship": "authentication" }SDK: fastVerifyJws(jws, relationship?) / fast_verify_jws(jws, relationship=None).
Without relationship, assertionMethod and then authentication are tried. The response has status (VALID, INVALID, UNSUPPORTED, INDETERMINATE or MALFORMED), valid, headline, alg, kid, typ, payload and signingInputSha256, plus signer and relationship when a key was found. A kid that is not a DID URL returns INDETERMINATE.
Example: a tool-call guard
Section titled “Example: a tool-call guard”The SDKs include a ready-made guard that also pins tool definitions: see Guard agent tools. The pattern below shows the same idea written by hand: deny by default, and allow a call only when DID.is says ALLOW.
import { DidisClient, DidisError } from "@didis/client";
const didis = new DidisClient();const TRUSTED_ROOTS = ["did:web:bank.example"];
export async function guardToolCall(chain: string[], tool: string): Promise<void> { try { const { authorization } = await didis.authorize(chain, tool, { trustedRoots: TRUSTED_ROOTS }); if (authorization.decision === "ALLOW") return; throw new Error(`denied: ${authorization.reason}`); } catch (e) { if (e instanceof DidisError && e.status === 429) { throw new Error(`rate limited, retry after ${e.problem.retryAfter ?? "?"} s`); } throw e; // any error means the call is not allowed }}authorize uses the intensive budget. For frequent checks, register the chain once with verifyDelegation(chain, { register: true }) and call verifyTool(agent, tool, root), which uses the standard budget.
Next steps
Section titled “Next steps”- Guard agent tools wraps tool handlers with the SDK guard.
- Agents and tools shows the same inspections in the web app.
- Delegation receipts describes the receipt format.