Inbound agents: check the agents that call your APIs
Overview
Section titled “Overview”Agent authorization governs the agents your organisation runs. Inbound agents covers the other direction: AI agents run by other organisations that call your APIs, MCP servers and data.
An agent enrolled in Shield360 can carry an agent passport in the Shield360-Agent header. A passport is a short-lived signed statement of:
- which agent is calling,
- which organisation runs it (the operator) and that organisation’s verified domain,
- when it acts for a person, that it does, as a pairwise id: stable for that person at your service, unlinkable across services, and never their name or email.
The Shield360 SDK verifier checks the passport where the call arrives. It decides under the policy you set here and reports each check to your project’s inbound decision log.
A passport proves who is calling. It never grants access by itself: your own API keys, OAuth tokens and permissions still decide what the call may do.
Inbound agents is part of the Security module. Open Govern → Agent access → Agent authorization and choose the Inbound agents tab (/agent-authorization?view=inbound). Everything on the tab belongs to the environment you are in: each environment (project database) has its own policy and log.
The receiver policy
Section titled “The receiver policy”| Field | Default | Meaning |
|---|---|---|
mode | observe | observe: verify and record every call, and never block one. The log shows what enforce would have denied. enforce: block calls the policy denies; the verifier answers 403. |
requireAgent | false | A call without a passport is denied (reason absent) and reported. Off: such calls are allowed and the verifier does not report them. |
operators | [] | The operators you accept, by organisation id or verified domain (for example acme.com). Empty: any verified operator. Others are denied (operator_not_allowed). Up to 100. |
requireOnBehalfOf | false | The passport must say the agent acts for a person (on_behalf_of_required). |
minAssurance | none | The least assurance you accept for the person behind the agent (see below). A weaker one, or none when one is required, is denied (assurance_too_low). |
online | false | Also ask the issuing Shield360, on every call, whether the passport is still good: the agent’s kill switch, its token, and that the passport was issued. Slower. If the issuer cannot be reached, the call is not denied for that reason. |
Assurance levels
Section titled “Assurance levels”When an agent acts for a person, the passport says how strongly it ties the agent to that person, strongest first:
| Level | Meaning |
|---|---|
delegated | The person, signed in, gave this agent a mandate. The passport names the mandate and when it ends. A mandate the person revoked is denied (mandate_revoked). |
named | A member of the agent’s organisation, named by the agent. |
unverified | A person named by the agent who is not a member of its organisation. |
minAssurance is none (the default: accept any call, whether or not it names a person), unverified, named or delegated. Each level also accepts the stronger ones.
A passport that is present but invalid (bad signature, expired, wrong audience, replayed and so on) is always denied. It is only blocked in enforce.
Start in observe. Look at the decision log and Operators seen for a while, list the operators you expect, then switch to enforce.
Changing the policy needs Manage agent authorization. Each change is in the audit log with the policy before and after. Verifiers pick a change up within a minute.
Install the verifier
Section titled “Install the verifier”In the environment that should receive the decisions, create an API key under Settings → API keys. Keep it in your service’s secrets, for example as SHIELD360_API_KEY.
Install the Shield360 TypeScript SDK and add the verifier to your API or MCP server. audience is your service’s host name: the one agents ask for a passport to.
import { agentVerifier } from "shield360/agents";
const verifier = agentVerifier({ audience: "api.example.com", policyFrom: { apiKey: process.env.SHIELD360_API_KEY!, baseUrl: "https://app.shield360.ai" }, report: { apiKey: process.env.SHIELD360_API_KEY!, baseUrl: "https://app.shield360.ai" },});
// Express / Connect: sets req.shield360Agent and answers 403 when enforce blocks.app.use(verifier.middleware());
// Or check one request yourself.const result = await verifier.check(request, { route: "/v1/jobs", method: "GET" });The Python SDK has the same verifier as shield360.agents.AgentVerifier, with ASGI and WSGI middleware.
Calls appear in the Inbound agents tab. The tab also shows this snippet with your Shield360 address filled in.
policyFrommakes the verifier follow the policy you set in the console. It caches the policy for 60 seconds. Leave it out to pass a localpolicyinstead.reportsends each result to the inbound decision log in batches. Reporting never blocks or slows a call. A verifier that cannot reach Shield360 keeps checking passports offline.- Passports are checked offline against the issuer’s public keys. Only the online check, when you turn it on, calls the issuer for each request.
The API the verifier uses
Section titled “The API the verifier uses”Both routes take the environment’s API key as Authorization: Bearer <key> and act on that key’s own environment. Nothing in the request can name another. Each key is rate limited.
GET /api/inbound-agents/policy answers the policy:
{ "mode": "observe", "requireAgent": false, "operators": [], "requireOnBehalfOf": false, "online": false, "minAssurance": "none" }POST /api/inbound-agents/decisions takes up to 100 check results:
{ "decisions": [ { "status": "verified", "decision": "allow", "blocked": false, "agent": { "id": "cm1agent", "name": "Booking agent", "status": "enrolled", "environment": "production", "risk": "high", "owner": { "domain": "acme.com" }, "reviewedAt": "2026-10-01T09:00:00.000Z", "enrolledAt": "2026-08-14T09:00:00.000Z" }, "operator": { "id": "cm1org", "name": "Acme", "domain": "acme.com" }, "onBehalfOf": { "id": "q3J5aV8tT0RkZ0ZxbFhB", "domain": "acme.com", "assurance": "delegated", "mandate": { "id": "cm1mandate", "expiresAt": "2026-11-28T00:00:00.000Z" } }, "scopes": ["jobs.read"], "issuer": "https://app.shield360.ai", "passportId": "6f1d3c1e-8a7b-4c2d-9e0f-123456789abc", "expiresAt": "2026-10-28T12:05:00.000Z", "at": "2026-10-28T12:00:01.000Z", "route": "/v1/jobs", "method": "GET", "receiver": "api.example.com" } ]}| Field | Rule |
|---|---|
status | verified, absent or invalid |
decision | allow or deny. An invalid result is always deny |
blocked | true only when decision is deny |
reason | Optional reason code, such as expired, operator_not_allowed, assurance_too_low, mandate_revoked, issuer_unreachable or replay_check_failed |
agent, operator, onBehalfOf | Optional. Ids of up to 128 characters, names of up to 200 |
onBehalfOf.assurance | Optional: delegated, named or unverified. onBehalfOf.mandate is optional { id, expiresAt } |
agent.status, agent.environment, agent.risk, agent.owner.domain, agent.reviewedAt, agent.enrolledAt | Optional, and each may be null. status is enrolled or not_enrolled; environment and risk are up to 64 characters; dates are ISO 8601 |
scopes | Up to 32, each [A-Za-z0-9_.:-], up to 64 characters |
at | When the call was checked: within the last 7 days and no more than 5 minutes ahead |
route, method, receiver | Optional. at, route, method and receiver may also be given once on the body for every result |
The answer is { "accepted": <n> }. Malformed JSON, more than 100 results, or any result with a bad field is a 400, and nothing in that batch is recorded. Too many requests for the key is a 429 with Retry-After. A bare array, or { "results": [...] }, is read the same way as { "decisions": [...] }.
What is recorded, and what never is
Section titled “What is recorded, and what never is”The verifier reports every call that carried a passport, and calls without one only when the policy denied them. Calls without a passport that were allowed are not reported, so the log and its counts are not a count of all your traffic.
Each check keeps:
- the status, decision, whether it was blocked, and the reason code,
- the agent’s id and name,
- the operator’s id, name and verified domain,
- the pairwise id and email domain of the person the agent acted for, the assurance level, and the mandate’s id and end when it was delegated,
- what the issuer said about the agent: enrolled or not, its environment and risk tier, its owner’s email domain, and when it was enrolled and last reviewed,
- the scopes the passport presented,
- the issuer, the passport id (
jti) and its expiry, - the method, the route’s path, the receiver, and when it was checked.
Never recorded: the passport itself, any request header (including Authorization and cookies), the request or response body, and the route’s query string, which is cut off before anything is stored. The agent’s stated purpose is checked but not kept.
Inbound decisions are kept for 400 days, like the agent authorization decision log, then deleted by the daily job.
Decision log, operators and export
Section titled “Decision log, operators and export”The tab shows:
- the last 30 days’ counts of reported checks: verified, no passport (denied ones only), invalid, denied and blocked,
- Operators seen in the last 30 days, with how many checks each operator’s agents made, how many were denied, how many named a person at each assurance level, and when they were last seen. An operator not on your allowed list is flagged,
- the inbound decision log, newest first, filtered by status, decision, assurance, operator, agent and period.
Export CSV and Export JSON download the filtered log, newest first, up to 10,000 records. When more records match, the export says it was cut short: the JSON has "truncated": true and the response carries X-Shield360-Truncated: true. Narrow the period to export the rest. CSV cells that start with =, +, - or @ are defused so a spreadsheet does not run them.
Exporting needs View agent authorization and Generate evidence packages. Each export is in the audit log with its filters, how many records it held and whether it was cut short.
Evidence
Section titled “Evidence”Inbound decisions count toward agent authorization evidence for ISO/IEC 42001:
- The control check Every AI agent has an owner, a purpose and a current access review (
agents.authorized) counts the period’s inbound decisions. It is applicable once an inbound decision has been recorded, even in an environment with no agents of its own. The finding says how many inbound calls were checked and denied, and from how many operators. It also says that decisions are what the verifiers reported, and that a call is only blocked where a verifier runs inenforce. - The evidence package carries the period’s inbound counts and the operators seen in
agent-authorization.json.
Permissions
Section titled “Permissions”| Action | Permission | Shipped roles |
|---|---|---|
| See the policy, the log and operators seen | View agent authorization | Administrator, Compliance officer, Analyst, Security |
| Change the policy | Manage agent authorization | Administrator, Security |
| Export the log | View agent authorization and Generate evidence packages | Administrator, Compliance officer |
Every route checks these on the server. The page only hides what you cannot do. Auditors and the read-only demo cannot change the policy.