The short answer
An agent ID is a signed statement anyone can check without asking the agent's owner: which agent this is, which company stands behind it, who it acts for, and, on each message it sends, that the message arrived unchanged. In Pidgeon the ID is an agent card, a small JWS signed under a key the agent's domain publishes, and each message or request the agent sends carries its own signature from the same domain. An API key says the caller may use your API; an agent ID says who the caller is.
Why an app cannot see who is behind an agent
Most agents run on credentials borrowed from a person: the owner's OAuth token, an API key made in the owner's account, a password in an environment variable. The app at the other end sees that person. It cannot tell the person from the agent, or one of their agents from another, and revoking the credential revokes all of them.
Email is no better. DKIM and DMARC say that a domain handled a message. They say nothing about whether a person or a program wrote it, or for whom, and a display name costs nothing to type.
So when an agent does something wrong with valid credentials, the app has a token and a timestamp. It has no answer to the question that matters: whose agent was this?
What an agent ID should prove
Four things, in this order:
- Who the agent is. A stable identifier and an address, so two agents from one company are two different things.
- Which company stands behind it. A domain the company has proved it controls, not a name it typed.
- Who it acts for. The organisation responsible for what the agent does.
- That the message is unchanged. A signature over what the agent sent: the recipients, the text and every attachment, or the HTTP request.
The first three belong to the agent and change rarely. The fourth belongs to each message. Post, the open protocol under Pidgeon, keeps them apart: a card carries the first three, and a signature on each message carries the fourth.
A worked example: the Post agent card
This is the card for support-agent@acme.com, an agent on a domain Acme has proved, decoded for reading:
{
"iss": "https://post.acme.com",
"sub": "8f0c…",
"iat": 1793782800,
"exp": 1793869200,
"v": "post0",
"address": "support-agent@acme.com",
"kind": "agent",
"on_behalf_of": "Acme Ltd",
"operator": { "domain": "acme.com", "verified": true },
"created_at": "2026-11-04T09:00:00Z",
"keys": [{ "type": "post", "domain": "acme.com", "kid": "…" }],
"reputation": { "standing": "new", "since": "2026-11" },
"url": "https://pidgeon.ai/api/v1/cards/8f0c…"
}
Each claim answers one of the four questions, or helps you weigh the answer:
subis the agent's id. It is never given to another agent, even if the address is deleted and created again, so it is the value to key an account on.kindisagent, orservicefor software that is not an agent, such as a workflow. Neverperson: a card says software is speaking.operatoris the domain the company proved, withverified: true.on_behalf_ofis the name the company set for itself, or its verified domain until it sets one. It is never taken from a person's email address.keysnames the key that signs the agent's mail, so a receiver knows what signature to expect.reputationis coarse on purpose:newfor the first 30 days, thenestablished, andrestrictedwhile the agent is suspended. Never a count.expis a day afteriat. Pidgeon signs a card when it is read rather than storing one, so nothing goes stale, and a copy somebody holds lapses on its own.
What a card never says: the account behind it, its members, its plan, a message count, or anything in the agent's inbox. It carries only what the agent's own mail already says, and facts about its domain.
How anyone checks it
The card is public, with no key:
curl https://pidgeon.ai/api/v1/cards/<id>
curl "https://post.acme.com/.well-known/post/card?address=support-agent@acme.com"
The answer holds jws, the signed card, and card, its payload decoded. The jws is the authority. To check it yourself, take the domain from iss, fetch https://post.acme.com/.well-known/post.json, find the key its kid names, verify the Ed25519 signature, then check exp and that address is the address you asked about. Any JOSE library can do this offline.
For a message, the verifier does all of it in one call. Send the raw message or its headers:
curl -X POST https://pidgeon.ai/api/v1/verify \
-H 'content-type: application/json' \
-d '{"message": "<the raw message, or its headers>", "recipient": "refunds@you.com"}'
The answer's verdict is verified, caution, failed or unsigned, with the sender, its operator and its card. On a good message the advice reads like this: "acme.com signed this as sent by an AI agent, and nothing it signed has changed. Act on it as you would on mail from acme.com." The verifier keeps nothing you send it. It checks 300 messages an hour per IP.
What an agent ID does not prove
- That the agent is right. A signature says who is accountable for a message, not that the message is true or should be obeyed. An agent's mail is still mail from outside.
- What the agent may do. Permissions stay with your own authorisation system. The card tells it who is asking.
- That the domain is honest. Anyone can register
acme-support.comand prove it. The verifier flags domains made to look like Pidgeon's own; a look-alike of your domain is not detected. Every card Pidgeon issues is in a public log, so you can watch it for cards that use your name. - The state of the last minute. A copy of a card is believed until its
exp, at most a day. When something matters, fetch it again.
When you do not need one
If your agents only call your own services with credentials you issued, your identity provider and your service accounts already answer who is calling. An agent ID earns its place when the agent reaches somebody who did not issue its credentials: a customer's inbox, a vendor's API, a sign-up form on someone else's site.
Start
Agents has the card's claims and how to issue, suspend and revoke one. Checking a message explains each verdict. Paste a message into the checker to see a card from the receiving side, or read Post, the draft that defines it.