When we built an A2A integration between Joule and a custom agent, the first thing our client code touched was not a task, a message, or a model — it was a small JSON file. The Agent Card is the entry point of every Agent2Agent conversation, and most integration bugs we see trace back to people treating it as a README-like afterthought instead of what it actually is: a machine-readable contract with normative rules attached. This guide walks the whole card — the URI it lives at, the semantics of every field group in the current v1.0 schema, how capability negotiation works against it, and what you are exposing when you publish it.
What the Agent Card Is — and Why It Is the Whole Contract
The A2A specification defines the Agent Card as the JSON metadata document an agent publishes so other agents can discover what it does, where it listens, how it speaks, and how it must be authenticated. Announced by Google in April 2025 and now stewarded under the Linux Foundation’s Agentic AI Foundation umbrella (original announcement), A2A deliberately keeps the negotiation surface in exactly one object. Before a client sends its first message, the card answers four questions:
- Reachability — which URLs and protocol bindings can carry traffic (
supportedInterfaces). - Behavior — what the agent is good at (
skills) and what operations it supports (capabilities). - Security — how the caller must authenticate (
securitySchemes,securityRequirements). - Identity — who stands behind the agent (
provider,version, and optionallysignatures).
Everything else in A2A — tasks, artifacts, streaming events — is negotiated per session. The card is negotiated once, cached, and trusted. If a client cannot fetch your card, your agent does not exist.
The /.well-known/agent-card.json URI
The spec’s discovery chapter adopts RFC 8615, the same mechanism that gives us /.well-known/security.txt: a fixed path suffix that clients can probe on any domain without configuration. The current standard location is:
https://{domain}/.well-known/agent-card.json
The path has version history you will meet in the wild. Through v0.2.6 the recommended suffix was /.well-known/agent.json (the old spec text is still readable in raw form); the rename to agent-card.json landed with v0.3.0 and stuck, and the specification registers the suffix as a permanent well-known URI whose change controller is the A2A Protocol Working Group. Permanent is the operative word: treat agent-card.json as a decade-scale API, which is also why the schema itself evolved field names (see supportedInterfaces below) instead of breaking the URI.
Three discovery strategies are sanctioned: the well-known URI for public agents, curated registries for enterprise catalogs, and direct configuration where a caller is handed the URL out of band. The spec pointedly does not standardize a registry API — registry selection is your governance decision, and the agent-discovery topic page only requires that whatever registry you use can serve or reference a valid card.
Cards change slowly, so the spec leans on HTTP caching: the server should send Cache-Control: max-age, and an ETag derived from the card’s version or a content hash; clients should honor RFC 9111 and revalidate conditionally with If-None-Match rather than re-downloading on every request. In practice:
$ curl -si https://sap-pr-approval.apps.cf.eu10.hana.ondemand.com/.well-known/agent-card.json | head -8
HTTP/2 200
content-type: application/json
cache-control: public, max-age=300
etag: "b7f9c1-1.4.0"
{"name":"SAP Purchase Requisition Approval Agent", ...}
A second-level 304 Not Modified on every poll after the first fetch is the behavior you want; if your CDN is serving a stale card to half your clients, that is a max-age tuning problem, not a protocol problem.
A Complete Agent Card, Field by Field
Here is a full v1.0 card for a purchase-requisition approval agent fronting SAP S/4HANA data — annotated inline. The authoritative shape is the protocol buffer definition, so we follow its required/optional split exactly.
{
// --- REQUIRED identity block ---
"name": "SAP Purchase Requisition Approval Agent", // display name, not an identifier
"description": "Answers questions about pending purchase requisitions, explains approval routing, and escalates stalled documents.", // short human/blurb prompt material
"version": "1.4.0", // version OF THE CARD; good ETag seed
"supportedInterfaces": [ // REQUIRED, ordered; first entry = preferred transport
{
"url": "https://sap-pr-approval.apps.cf.eu10.hana.ondemand.com/a2a/jsonrpc", // absolute HTTPS in production
"protocolBinding": "JSONRPC", // JSONRPC | GRPC | HTTP+JSON, extensible
"protocolVersion": "1.0" // v1.0 moved this from card root to per-interface
},
{
"url": "https://sap-pr-approval.apps.cf.eu10.hana.ondemand.com/a2a/rest/",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "0.3" // same agent can expose mixed versions per interface
}
],
"capabilities": { // REQUIRED; false-by-default promises
"streaming": true, // else SendStreamingMessage must fail
"pushNotifications": false, // else push-config ops must fail
"extendedAgentCard": true, // authenticated richer card exists
"extensions": [
{
"uri": "https://exa.example/sap/purchase-order-extension/v1", // REQUIRED — clients opt in via A2A-Extensions header
"description": "Adds SAP EMEA purchasing conventions and policy references to task metadata.",
"required": true // client that does not support this must refuse the task
}
]
},
"defaultInputModes": ["text/plain", "application/json"], // REQUIRED; Media Type strings
"defaultOutputModes": ["text/plain", "application/json"], // REQUIRED
"skills": [ // REQUIRED, non-empty: descriptive, not callable tools
{
"id": "pr-status", // REQUIRED; stable across card versions
"name": "Requisition status lookup", // REQUIRED
"description": "Looks up a purchase requisition by number and explains its approval state.", // REQUIRED; honest granularity beats a heroic summary
"tags": ["sap", "procurement", "approvals"], // REQUIRED; discovery/search keywords
"examples": ["Where is requisition 450000123?", "Why is my PR stuck in approval?"],
"inputModes": ["text/plain"], // OPTIONAL overrides of the card defaults
"securityRequirements": [ // OPTIONAL per-skill tightening
{ "schemes": { "entra": { "list": ["PrPurchaseGroup.Read"] } } }
]
}
],
// --- OPTIONAL block ---
"provider": {
"organization": "Contoso Retail AG", // both provider fields required together
"url": "https://www.contoso.example"
},
"documentationUrl": "https://wiki.contoso.example/a2a/pr-approval",
"iconUrl": "https://cdn.contoso.example/img/pr-agent.png",
"securitySchemes": { // named schemes; v1.0 uses oneof wrappers, not "type"
"entra": {
"httpAuthSecurityScheme": { "scheme": "bearer", "bearerFormat": "JWT", "description": "Entra ID token" }
},
"sapias": {
"openIdConnectSecurityScheme": { "openIdConnectUrl": "https://authz.contoso.example/.well-known/openid-configuration" }
}
},
"securityRequirements": [ // v0.3 called this field `security`
{ "schemes": { "entra": { "list": [] } } } // scope lists wrapped in {"list": [...]}
],
"signatures": [ // RFC 7515 JWS over RFC 8785-canonicalized card
{ "protected": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpPU0UifQ", "signature": "MEUCIQ..." }
]
}
Three field-level traps worth naming. First, version is the card’s version — clients key caches on it, so bump it whenever anything material changes. Second, the eight required fields leave no “happy path defaults”: a card without skills is invalid even if the agent is a single-purpose bot, so publish one honest skill rather than none. Third, v1.0 renamed the flat security field: what v0.3 published as security became securityRequirements, and each scheme’s scopes moved inside a {"list": [...]} wrapper because the underlying protobuf oneofs cannot express bare arrays — the v1.0 change log tracks the renames if you are migrating.
Capability Negotiation: Interfaces, Versions, Extensions
The card is where the transport conversation starts, and v1.0 centralized it. Earlier revisions spread endpoints over url, preferredTransport, and additionalInterfaces; v1.0 consolidated all of that into the ordered supportedInterfaces list, with each entry binding a URL to a protocolBinding (JSONRPC, GRPC, HTTP+JSON, or something mutually agreed) and its own protocolVersion. The client rules from the spec are simple: parse the list, pick the first interface whose binding you support, and prefer earlier entries — the first item is the server saying “use me”.
Two subtleties follow. Because protocolVersion sits on the interface, a server can expose a v1.0 JSON-RPC endpoint and a legacy v0.3 REST endpoint in one card, and a client pinned to v0.3 can still find its path. And when an agent fronts several downstream agents, supportedInterfaces entries may carry a tenant field — an opaque routing identifier the client must echo verbatim in every subsequent request; drop it and you get the wrong backend.
On the wire, the negotiated version travels in the A2A-Version header as Major.Minor, and the extension opt-in is a comma-separated list of extension URIs:
POST /a2a/jsonrpc HTTP/1.1
A2A-Version: 1.0
A2A-Extensions: https://exa.example/sap/purchase-order-extension/v1
Unsupported versions must fail with VersionNotSupportedError. The declared booleans then act as gates the server must enforce against its own card: calling SendStreamingMessage or SubscribeToTask when capabilities.streaming is false or absent must return UnsupportedOperationError; push-notification configuration against pushNotifications: false must return PushNotificationNotSupportedError; requesting the extended card when extendedAgentCard is false must fail as unsupported, and when it is declared true but not configured, with ExtendedAgentCardNotConfiguredError. A server that honors streaming despite a false flag is non-conformant, not generous — clients cache the card and never retry the “unsupported” call.
Extensions add a second negotiation axis. Each AgentExtension advertises a uri, optional description, params, and a required flag. Clients opt in by listing extension URIs in A2A-Extensions; if a card marks an extension required: true and the client does not implement it, the server refuses the task with ExtensionSupportRequiredError. That flag is powerful — mark an extension required only when the agent genuinely cannot behave correctly without it, because you are shrinking your own client pool.
Skills: The Discovery Surface
Skills are the fields most likely to be misread. An AgentSkill requires id, name, description, and tags, and may add examples, per-skill inputModes/outputModes, and per-skill securityRequirements. The spec’s own framing is that skills describe what the agent is “likely to succeed at” — they are discovery and routing hints for client-side LLMs deciding which agent to delegate to, not invokable tool definitions. Nothing in A2A executes a skill by ID. The id must be stable across card versions because clients and registries index on it; the tags are what registry search matches; the examples are exactly that — sample phrasings, useful because modern clients embed skill descriptions in router prompts.
Per-skill inputModes/outputModes override the card defaults, which is how one agent legitimately offers a text-only skill beside a text/plain default. Per-skill securityRequirements tighten, never relax: a caller authenticated against the card-level default still needs the scopes that specific skill lists. And when capabilities.extendedAgentCard is true, expect more and richer skills behind the authenticated endpoint — that is the mechanism the spec gives you for offering more once the caller’s identity is known.
We would rather read twelve narrow, honest skills than one paragraph claiming the agent “handles all procurement matters”. Overstated descriptions do not fail loudly; they fail as rejected tasks and confused users one delegation at a time.
Security of the Card Itself
Everything above assumed you want the card public. RFC 8615 paths are crawlable by construction, so treat the public card as a broadcast: the discovery guidance is blunt that you should not expose internal or restricted URLs and sensitive capability details there. Gate the endpoint itself when content is sensitive — mutual TLS via the mtlsSecurityScheme wrapper, IP allow-lists, or move the full picture into the authenticated extended card, which the server must protect with one of the card’s own declared schemes and may vary by caller identity. Registry-based discovery offers a middle path: selective disclosure per client identity is exactly what enterprise registries are for.
On credentials, the rule is absolute: never embed static secrets in a card, and prefer out-of-band dynamic credentials. Schemes like apiKeySecurityScheme (with location of header, query, or cookie), httpAuthSecurityScheme, oauth2SecurityScheme — which can point at an OAuth metadata document via oauth2MetadataUrl per RFC 8414 — describe how callers authenticate; the actual tokens come from your IdP, never from the card.
The signatures field closes the authenticity gap: each entry is a detached RFC 7515 JWS computed over the card canonicalized with RFC 8785 JSON Canonicalization Scheme, with signatures itself excluded from the signed content so a signed card stays valid under byte-order variation. Verification lets a client detect tampering in transit or in a cache and confirm the publishing organization.
Real deployments show the trade-offs. Azure AI Foundry serves A2A cards from custom per-version paths (.../protocols/a2a/agentCard/v1.0 and v0.3) rather than the well-known URI, and — tellingly — does not support anonymous card retrieval: every A2A URL including the card requires Entra ID, with the caller holding the Foundry Agent Consumer role. A governed enterprise can reasonably decide that even the advertisement is not public; the protocol supports both extremes.
What This Guide Does Not Cover
Deliberately out of scope:
- Task lifecycle, streaming, and push notifications — the message semantics after negotiation. Covered in our A2A integration tutorial and the spec’s operations chapter.
- MCP relationship — why tool-level context (see the Model Context Protocol, which standardizes no equivalent server-discovery document) and agent-level delegation compose; the A2A-and-MCP topic does it justice.
- Registry APIs and governance — the spec intentionally leaves registries unstandardized; picking or building one is an enterprise decision.
- Signature implementation — JCS canonicalization and JWKS distribution deserve their own walkthrough.
- Host-side agent wiring — Joule, M365 Copilot, and Foundry agent-service configuration live in this SAP Joule tutorial.
FAQ
Do I need to serve /.well-known/agent.json or /.well-known/agent-card.json?
Serve agent-card.json — that is the standard from v0.3.0 onward, permanent per the spec’s registration. Keep agent.json only if you know a v0.2-era client exists, as a compatibility copy of the same content. Do not let the two drift: publish one card and project both paths, ideally from CI, because a stale duplicate is worse than no duplicate.
Which field tells a client which protocol version to request?
Per-interface protocolVersion inside each supportedInterfaces entry (v1.0). The client picks a supported binding, notes that entry’s version, and echoes it in the A2A-Version header as Major.Minor. Cards from v0.3 and earlier put one version at the root with url/preferredTransport; if you maintain both card shapes, keep the mapping deliberate, since the fields consolidated in v1.0.
Can the URLs in my Agent Card be private or internal?
Yes, but only where the audience is private. A card at a public well-known URI advertising internal hostnames hands attackers a map, which is why the spec pushes sensitive endpoints into the authenticated extended card or registry-based distribution. If your clients are all inside a network boundary, prefer direct configuration: skip the public URI entirely rather than gate it.
Is signing the Agent Card mandatory, and what does it actually protect?
No — signatures is optional and the protocol functions without it. What it buys is authenticity and tamper-evidence: a JWS over the canonicalized card lets a client verify the document came from the named provider and was not modified in a cache or proxy. It does not protect confidentiality, and a self-signed signature without a trust anchor is ceremony, so sign when registries or clients have a way to fetch your verification keys.
References
- A2A Specification (v1.0) — Agent Card schema, discovery, negotiation, security chapters: https://a2a-protocol.org/latest/specification/
a2a.proto— authoritative protobuf definitions for AgentCard/AgentSkill/AgentCapabilities: https://raw.githubusercontent.com/a2aproject/A2A/main/specification/a2a.proto- Specification, full markdown (raw): https://raw.githubusercontent.com/a2aproject/A2A/main/docs/specification.md
- A2A spec v0.2.6 (well-known
agent.jsonera): https://raw.githubusercontent.com/a2aproject/A2A/v0.2.6/docs/specification.md - What’s new in v1.0 (field renames and consolidations): https://a2a-protocol.org/latest/whats-new-v1/
- Agent discovery topic — strategies, caching, sensitivity guidance: https://a2a-protocol.org/latest/topics/agent-discovery/
- A2A key concepts: https://a2a-protocol.org/latest/topics/key-concepts/
- A2A and MCP compared: https://a2a-protocol.org/latest/topics/a2a-and-mcp/
- Google’s A2A announcement (April 2025): https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/
- RFC 8615 — Well-Known URIs: https://www.rfc-editor.org/rfc/rfc8615
- RFC 7515 — JSON Web Signature: https://datatracker.ietf.org/doc/html/rfc7515
- RFC 8785 — JSON Canonicalization Scheme: https://datatracker.ietf.org/doc/html/rfc8785
- RFC 9111 — HTTP Caching: https://datatracker.ietf.org/doc/html/rfc9111
- RFC 8414 — OAuth 2.0 Authorization Server Metadata: https://datatracker.ietf.org/doc/html/rfc8414
- Azure AI Foundry — enable A2A endpoints and card paths: https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/enable-agent-to-agent-endpoint
- Model Context Protocol documentation: https://modelcontextprotocol.io/docs/getting-started/intro
The full A2A series continues in our integration walkthrough; browse more in the A2A category or the tutorial index.