The Resolution API
An AI agent just showed up on your platform. One GET request answers the three questions that matter before you let it transact: is it who it claims to be, is it acting inside a human mandate, and what is its record. Checking a single agent by hand is free forever. This is the same answer for software, at volume, metered.
The first 1,000 checks each month cost nothing. After that, $0.02 per answered check. No seats, no tiers. Add the MCP server and your own agent can ask the same questions.
Is it who it claims to be?
The signature on the credential is recomputed against the key published at apir.ai on every call. Never a stored flag, never a cached answer.
Is it acting inside a human mandate?
What a named human principal actually permitted: allowed actions, value cap, expiry, and whether the grant itself is signed. When authority was delegated agent to agent, the full chain resolves back to the human at the root.
What is its record?
How much sealed, hash-chained activity stands behind the claim, and when the agent last acted. A count of events on record, not a promise.
One endpoint, one question
GET /api/v1/resolve?q=… takes an agent name, a credential ID (TP-…), or an agent UUID. Names resolve against public credentials only: a private credential never surfaces from a guessed name, it resolves only if you already hold its exact ID.
curl "https://apir.ai/api/v1/resolve?q=TP-EXAMPLE0-00000000" \ -H "Authorization: Bearer apir_sk_your_key"
{
"verdict": "VERIFIED",
"query": "TP-EXAMPLE0-00000000",
"checked_at": "2026-08-13T04:20:11.000Z",
"agent": { "name": "Example Agent", "status": "active" },
"identity": {
"verified": true,
"passport_id": "TP-EXAMPLE0-00000000",
"key_id": "apir-pp-XXXXXXXXXXXX",
"algorithm": "Ed25519",
"issuer": "did:web:apir.ai"
},
"authority": {
"reference": "MND-XXXXXXXXXX",
"granted_by": "an agent",
"allowed_actions": ["memo.draft"],
"value_cap": 0,
"expires": "2026-11-05T00:00:00.000Z",
"signed": true,
"chain": {
"verdict": "INTACT",
"depth": 2,
"human_at_root": true,
"break_reason": null,
"hops": [
{ "depth": 1, "mandate_ref": "MND-XXXXXXXXXX", "principal_type": "agent",
"allowed_actions": ["memo.draft"], "value_cap": 0, "state": "ok", "signed": true },
{ "depth": 2, "mandate_ref": "MND-YYYYYYYYYY", "principal_type": "human",
"allowed_actions": ["memo.draft", "work.route"], "value_cap": 0, "state": "ok", "signed": true }
]
}
},
"record": {
"sealed_events": 1606,
"last_activity": "2026-08-12T22:04:00.000Z"
},
"credential": {
"trust_score": 83,
"trust_grade": "B",
"risk_tier": "low",
"issued_at": "2026-08-07T00:00:00.000Z",
"expires_at": "2026-11-05T00:00:00.000Z",
"public": true
},
"verify_yourself": {
"published_keys": "https://apir.ai/.well-known/jwks.json",
"note": "This answer is not signed. To check a public passport yourself, fetch it by passport_id from https://apir.ai/api/verify-passport and recompute its signature against the published key."
},
"billing": { "metered": true, "month_checks": 412, "free_tier": 1000 }
}Field names and types above are exactly what the endpoint returns. Missing auth returns 401 with instructions. A missing q returns 400. You are only billed for answered questions, and an honest NO_RECORD counts as an answer because it is one. Every response carries billing.month_checks, so you can watch your own consumption without asking us for it.
HTTP/1.1 402 Payment Required
{
"error": "free_tier_exhausted",
"month_checks": 1001,
"free_tier": 1000,
"how": "The first 1,000 checks each month are free. Attach billing at https://apir.ai/billing to keep resolving at $0.02 per check.",
"free_alternative": "Checking a single agent by hand stays free at https://apir.ai/check-agent, no key needed."
}With billing attached there is no cap here: the graduated meter is what regulates the account, and checks simply keep answering. Without it, check 1,001 in a calendar month is refused rather than silently throttled or silently charged, and the refusal tells you exactly how to continue.
Every way this can say no
Each one carries the reason and the way forward in its own body. You should never have to learn a limit by hitting it in production.
| Code | What happened | Body | What to do |
|---|---|---|---|
| 400 | Missing question | { error: 'Pass ?q= an agent name, credential id, or agent uuid' } | Send a q parameter. There is nothing to look up without one. |
| 401 | No key, or a dead key | { error: 'A valid API key is required', how: '…', free_tier: '…' } | Send Authorization: Bearer apir_sk_… The body tells you where keys come from and reminds you the single check is free. |
| 402 | Free tier exhausted, no billing attached | { error: 'free_tier_exhausted', month_checks: 1001, free_tier: 1000 } | Attach billing. With billing attached there is no cap here at all: the graduated meter regulates the account instead. |
| 429 | Rate limited | { error: 'Too many checks, give it a minute' } | Back off and retry. The limiter fails open, so a limiter outage never takes the endpoint down. |
| 500 | We failed | { error: 'Resolution failed, try again' } | Retry. A failure on our side is never reported as an agent being unverified, which is why UNCHECKED exists as its own verdict. |
Give your agent the ability to check a stranger
An MCP server at https://apir.ai/api/mcp. Add it to any MCP client and your agent can ask the three questions before it transacts with an agent it did not build. Read only, unauthenticated, and free, exactly like the human check. No tool here can issue, revoke, send or spend.
verify_agent
Is this agent who it claims to be? Returns one of seven verdicts, each with what it means and what it does not.
check_authority
What did a named human actually permit? Resolves the delegation chain back to the person at the root.
get_verification_keys
The published Ed25519 keys, so a signature can be recomputed offline without trusting any answer we give.
{
"mcpServers": {
"apir-verify": {
"type": "http",
"url": "https://apir.ai/api/mcp"
}
}
}When an agent sends another agent
An agent arrives holding a mandate. That mandate was granted by another agent, which was granted by another, and somewhere at the top a person clicked approve. The mandate in front of you says nothing about whether the agent that granted it had the right to.
Every response resolves the whole chain. One law is enforced at every hop: authority may only ever narrow. No hop can hold an action, a target, or a spending cap larger than the hop above it, no hop may outlive its parent, and the root must be a human. Revoke any link and every mandate beneath it stops resolving on the very next call.
INTACT
Every hop active, unexpired, narrower than its parent, human at the root.
BROKEN
A hop is revoked, expired, wider than its parent, or the chain roots in an agent. break_reason names which.
UNRESOLVED
The chain could not be walked on this call. Never reported as intact.
Each hop carries its own signature and key id, so you can recompute every link offline against the published key rather than taking our word for the walk.
Seven answers, none of them vague
Every verdict carries checked_at, the instant it was made. A verification is true as of a moment, and that moment is always in the payload.
VERIFIED
The Ed25519 signature recomputes against the published key, the credential is unexpired and unrevoked. Stamped with the instant it was checked: a verdict is true as of a moment, never permanently.
EXPIRED
The signature is genuine but the credential is past its expiry. History, not a live credential.
REVOKED
The issuer or the operator withdrew this credential. It no longer vouches for anything.
UNSIGNED
A record exists but its signature is missing or does not recompute. The contents cannot be vouched for.
UNCHECKED
The signature could not be checked on this call. That is our problem, not a mark against the agent, and it is never reported as verified.
SUPERSEDED
Genuine, unexpired, and the signature still recomputes, but the issuer has since replaced it with a newer credential. Resolve the agent rather than this credential id to reach the current one.
NO_RECORD
Nothing is on file. That is not evidence of wrongdoing and it is not a warning. It means any claim the agent makes about itself is unverified.
Keys are self-serve. Create a free account, open Settings → API Keys, and create one. The raw key is shown once and never stored: we keep only its SHA-256 hash. Send it as Authorization: Bearer apir_sk_…
1,000 checks per month free, then $0.02 per check. The price scales with your transaction volume, not with seats. The human check at /check-agent stays free forever and is never paywalled. That split is the whole model: the party who needs the answer at volume is the party the meter runs for.
Your agent runs where it runs. Register it at /agents/connect (the first agent is free), keep the key it gives you, and after each thing the agent does, send one call. Every action goes into the same insert-only, hash-chained log book as APIR’s own agents, and is timestamped nightly by an outside RFC 3161 authority. Prompts and outputs never leave your side: send their SHA-256 hashes. The log book proves what the agent reported and shows any later change to it, not that the report was complete.
POST /api/v1/events
curl -X POST https://apir.ai/api/v1/events \
-H "Authorization: Bearer apir_sk_..." \
-H "Content-Type: application/json" \
-d '{"agent_id":"<the agent id>",
"events":[{"type":"decision",
"summary":"Approved refund #4521 under the AUD 200 limit",
"input_hash":"<sha256 of the prompt>",
"output_hash":"<sha256 of the answer>",
"data":{"tool":"refunds"}}]}'
# types that count as conduct: action, decision, tool_call
# (also recorded: input, output, reasoning, data_access, error).
# Up to 50 events a call.
# Answer: each event's sequence number and chain hash.Five conduct events and the trust genome can score the agent; then its owner writes its mandate and issues its papers. Anyone checks the result at /check-agent, free.
From Python: pip install git+https://github.com/apir-ai/apir-record. No dependencies, MIT. It hashes inputs and outputs on your side and checks each event against these rules before it sends.
POST /api/v1/agents
Got a fleet? Register them in one call instead of one form each. You need a key made in Settings, API keys, with “register agents” ticked.
curl -X POST https://apir.ai/api/v1/agents \
-H "Authorization: Bearer apir_sk_..." \
-H "Content-Type: application/json" \
-d '{"agents":[{"name":"Refunds agent","platform":"your-platform","model":"your-model"},
{"name":"Support agent"}]}'
# Up to 50 a call.
# All or nothing: if they don't all fit your allowance, none are registered (402).
# A name already on the record comes back under "existing", so a retry can't double up.
# Answer: "created" and "existing", each with the agent's id.
# One key with agent:write can send events for every agent in your organisation.AG-UI agents
If your agents speak AG-UI (LangGraph, CrewAI, Mastra, Pydantic AI, Google ADK and others), add one subscriber to your AG-UI client and each tool call and finished run goes to the log book. It hashes arguments and results on your side, needs no packages, and never throws into your agent. Run it where your key is safe: on a server, never in a browser. Built against the AG-UI spec as published on 7 November 2026; it has not yet been run against a live AG-UI agent, so tell us what you find.
// Your AG-UI client agent: HttpAgent, or your own AbstractAgent.
import { apirSubscriber } from './apir-ag-ui.mjs'; // https://apir.ai/sdk/apir-ag-ui.mjs
agent.subscribe(apirSubscriber({
apiKey: process.env.APIR_API_KEY, // a key with agent:write, kept on your server
agentId: '<the agent id>',
}));
// Each tool call: tool_call, with SHA-256 of its arguments and of its result
// Each run: output when it finishes, error when it fails
// Sent in batches; anything left is sent when the run ends.The file: apir.ai/sdk/apir-ag-ui.mjs. One file, MIT.
Both are free for a person on the site. By API they are one fixed figure per act, on the rate card, drawn from your organisation’s credits on the call. A call the service cannot complete is refunded on the spot. Neither carries a score, an estimate, or a finding APIR cannot make.
POST /api/v1/seal
curl -X POST https://apir.ai/api/v1/seal \
-H "Authorization: Bearer apir_sk_..." \
-H "Content-Type: application/json" \
-d '{"declarant_name":"Ana Ruiz","kind":"artwork",
"method":"human_made","title":"Harbour, oil on linen",
"statement":"Painted by me over March and April 2026, no generative AI.",
"files":[{"name":"harbour.tif","size":48213000,"sha256":"<64 hex>"}],
"confirmed":true}'Hash the files on the declarant’s side; they never arrive. The answer carries the seal id and its public page. The sentence on it stays locked: APIR sealed the declaration and the files; it did not determine how the work was made.
GET /api/v1/profile?q=
curl "https://apir.ai/api/v1/profile?q=48638569632" \ -H "Authorization: Bearer apir_sk_..." # q: a business name, an ABN, an ACN or a website. # Answer: entity from the register, the site's own # facts, domain dates, every source named connected # or not, and the date each was read.
The same dossier a person sees at /profile, with the same two sentences on it. Prices for both are on the rate card.
Passports are signed with Ed25519 under did:web:apir.ai. You can fetch one in the W3C Verifiable Credential shape, but the proof is APIR’s own scheme, so a general VC library will not verify it. Our public keys are published at apir.ai/.well-known/jwks.json and retired keys stay published so old credentials still verify. To recompute a public passport’s signature offline you need that key and the passport’s public answer, which prints all eleven signed fields. Whether a passport is still current is read live from us: there is no signed revocation list yet. To check an exported log book, an agent’s stamp or a seal on your own machine, the apir-record checker is open source. The specification lists the known gaps.
Answer the question before the agent transacts