Skip to content

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.

AI agentdelegation chain
DID.is checks
  • Each receipt signature and its issuer DID
  • Links: each step narrows the previous one
  • Dates and trusted root
  • The leaf capabilities grant this tool
Your gatewayALLOW / DENY + reason
  • 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:transfer grants only payments:transfer), or ns:* for a tool named ns:… (payments:* never grants refund or slack: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 any signatures[] are verified.
  • Agent behaviour. Nothing here observes or limits what the agent does after a call is allowed.
  • Server identity. MCP serverInfo is self-reported; MCP has no server identity attestation.
  • Tool safety. heuristicClass is a DID.is estimate from names, descriptions and schemas; annotations are the server’s own claims.
  • Standard status. Delegation receipts are a DID.is experimental format, not a ratified standard.
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.

POST /v1/agents/authorize verifies the chain and checks one tool against the leaf’s capabilities. It is stateless: nothing is stored.

Terminal window
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"]}'

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.

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.

Send receipts root first.

Terminal window
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."
]
}
}

After registering a chain with register: true, ask about an agent without sending the chain again:

Terminal window
curl 'https://did.is/api/v1/agents/verify-tool?agent=did:web:agent.example&tool=payments:transfer'
{
"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.

Terminal window
curl 'https://did.is/api/v1/mcp/inspect?endpoint=https://mcp.deepwiki.com/mcp'

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.
Terminal window
curl 'https://did.is/api/v1/a2a/inspect?url=https://agent.example.com'

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.

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.

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.