An Agent Card is the JSON document an A2A agent publishes to say who it is, where and how to call it, what it can do, and how callers must authenticate. Clients usually fetch it from https://{domain}/.well-known/agent-card.json, though registries and direct configuration also work. Every client decision before the first message, from which endpoint to use to which token to obtain, is driven by fields in this document.
This page is a reference to those fields as defined by A2A 1.0. The normative definition is specification/a2a.proto in the A2A repository, package lf.a2a.v1. The JSON form uses the protobuf JSON mapping, so supported_interfaces in the proto becomes supportedInterfaces on the wire. For each field you get its type, whether it is required, what a client does with it, and the mistakes that show up in real cards. A worked card, a selection routine, a lint script and a signature verifier make it concrete. How to publish, cache and govern cards over their lifetime is covered in Agent Card architecture.
The card at a glance
| Field | Type | Required | Purpose |
|---|---|---|---|
name | string | yes | Human-readable agent name |
description | string | yes | What the agent does; read by people and by routing models |
supportedInterfaces | AgentInterface[] | yes | Endpoints, bindings and protocol versions, in preference order |
provider | AgentProvider | no | Organization and URL of the operator |
version | string | yes | Version of the agent, not of the protocol |
documentationUrl | string | no | Human documentation |
capabilities | AgentCapabilities | yes | Optional protocol features the agent supports |
securitySchemes | map of SecurityScheme | no | Named authentication methods |
securityRequirements | SecurityRequirement[] | no | Which schemes and scopes a caller must satisfy |
defaultInputModes | string[] | yes | Media types accepted, unless a skill overrides |
defaultOutputModes | string[] | yes | Media types produced, unless a skill overrides |
skills | AgentSkill[] | yes | The focused tasks the agent is good at |
signatures | AgentCardSignature[] | no | JWS signatures over the canonical card |
iconUrl | string | no | Icon for catalogues and UIs |
Here is a complete, valid 1.0 card for a finance agent. The sections below refer back to it.
{
"name": "Invoice Reconciliation Agent",
"description": "Matches supplier invoices to purchase orders and explains mismatches.",
"version": "2.4.0",
"supportedInterfaces": [
{"url": "https://agents.example.com/recon/a2a", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"},
{"url": "https://agents.example.com/recon/rest", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0"}
],
"provider": {"organization": "Example Finance Platform", "url": "https://example.com"},
"documentationUrl": "https://docs.example.com/agents/recon",
"capabilities": {"streaming": true, "pushNotifications": false, "extendedAgentCard": true},
"securitySchemes": {
"corp-oauth": {
"oauth2SecurityScheme": {
"flows": {"clientCredentials": {
"tokenUrl": "https://auth.example.com/oauth2/token",
"scopes": {"recon.read": "Read reconciliations", "recon.run": "Start reconciliations"}}}
}
}
},
"securityRequirements": [{"schemes": {"corp-oauth": {"list": ["recon.run"]}}}],
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["application/json"],
"skills": [
{
"id": "reconcile-invoice",
"name": "Reconcile an invoice",
"description": "Given an invoice number or PDF, finds the matching purchase order and lists line-level differences.",
"tags": ["finance", "invoices", "reconciliation"],
"examples": ["Reconcile invoice INV-20931 against its PO."],
"inputModes": ["text/plain", "application/pdf"]
}
]
}
Identity fields
name and description are required. The description does more work than it appears to. Registries index it, and orchestrating agents often choose a delegate by passing candidate descriptions to a model. A description that states inputs, outputs and limits in one or two sentences routes better than marketing copy.
version is the agent's own version, such as 2.4.0. It is unrelated to the protocol version, which lives on each interface. Bump it whenever the card's meaning changes. The specification suggests deriving the card's HTTP ETag from this field or from a hash of the content, so a stale version number leads to stale caches.
provider is optional, but if present both organization and url are required. documentationUrl and iconUrl are optional and purely descriptive. Nothing in them should be needed to make a call.
supportedInterfaces: where and how to call
In A2A 1.0 this list replaces the single url of earlier versions. Each AgentInterface has four fields.
| Field | Required | Meaning |
|---|---|---|
url | yes | Absolute HTTPS URL for HTTP bindings in production; host:port for gRPC |
protocolBinding | yes | Open string; the core values are JSONRPC, GRPC and HTTP+JSON |
protocolVersion | yes | Major.Minor of A2A exposed here, such as 1.0 or 0.3 |
tenant | no | Opaque routing value the client must copy into every request message |
Order carries meaning: the first entry is the agent's preferred interface. A client must walk the list and pick the first entry whose binding and version it supports, use that entry's URL, and set the tenant field of every request to exactly the declared value, or omit it if none is declared. Clients send the protocol version in an A2A-Version header, or as a request parameter, on every request. A server must treat an empty value as 0.3 and must reject versions it does not support with VersionNotSupportedError. Versions are always Major.Minor, and patch numbers are never used in negotiation.
SUPPORTED = {("JSONRPC", "1.0"), ("HTTP+JSON", "1.0")} # what this client implements
def pick_interface(card):
"""First entry the client supports wins: the list is in the agent's preference order."""
for iface in card["supportedInterfaces"]:
if (iface["protocolBinding"], iface["protocolVersion"]) in SUPPORTED:
return iface
raise RuntimeError("no mutually supported interface")
def request_headers(iface, token):
return {"A2A-Version": iface["protocolVersion"], # an empty header means 0.3
"Authorization": f"Bearer {token}"}
# If iface has a "tenant", copy it verbatim into the tenant field of every request message.Because each interface declares its own version, one card can advertise a 1.0 JSON-RPC endpoint and keep a 0.3 endpoint for older clients during a migration. A2A versioning covers running both.
capabilities and extensions
| Field | Type | Meaning |
|---|---|---|
streaming | optional bool | The agent supports streaming responses |
pushNotifications | optional bool | The agent can send push notifications for task updates |
extendedAgentCard | optional bool | An authenticated caller can fetch a fuller card |
extensions | AgentExtension[] | Protocol extensions the agent supports |
The object itself is required even if every flag is false. Each flag gates an operation, so clients must check it before trying. If extendedAgentCard is false or absent, a call to the GetExtendedAgentCard operation must fail with UnsupportedOperationError. If it is true but nothing is configured, the agent returns ExtendedAgentCardNotConfiguredError. Over JSON-RPC the method is GetExtendedAgentCard; over the HTTP+JSON binding it is GET /extendedAgentCard. The extended card typically lists skills or interfaces that the public card omits.
An AgentExtension has a uri that identifies it, a description, a boolean required and free-form params. Setting required to true means a client that does not understand the extension should not call the agent. Use it sparingly, because every required extension shrinks the set of clients that can talk to you.
securitySchemes and securityRequirements
securitySchemes is a map from a name you choose to a scheme. In 1.0 each scheme is a protobuf oneof, so in JSON the scheme is wrapped in a key naming its kind. That is the most common mistake in cards written from 0.3 examples, which used a flat object with a type field.
| Wrapper key | Required fields | Notes |
|---|---|---|
apiKeySecurityScheme | location, name | location is query, header or cookie; 0.3 called it in |
httpAuthSecurityScheme | scheme | For example Bearer; optional bearerFormat |
oauth2SecurityScheme | flows | One of authorizationCode, clientCredentials, deviceCode; implicit and password are deprecated; optional oauth2MetadataUrl |
openIdConnectSecurityScheme | openIdConnectUrl | OIDC discovery document URL |
mtlsSecurityScheme | none | Mutual TLS; only an optional description |
securityRequirements is a list, and each entry has a schemes map from scheme name to a list of scopes wrapped as {"list": [...]}. The semantics follow OpenAPI: the list is an OR of ANDs. A caller must satisfy every scheme inside at least one entry. So two entries, one naming OAuth and one naming an API key plus mTLS, mean 'OAuth, or an API key over mutual TLS'. Skills can carry their own securityRequirements, which is how one agent exposes a read-only skill to a wide audience and a write skill to a narrow one. A2A authentication covers the token flows behind these declarations.
Input and output modes, and skills
defaultInputModes and defaultOutputModes are media types, such as text/plain, application/json or image/png. They are required and act as defaults for every skill.
Each AgentSkill has a required id, name, description and tags, and optional examples, inputModes, outputModes and securityRequirements. A skill's modes replace the defaults rather than adding to them. In the worked card the reconciliation skill accepts PDFs, which the agent-wide defaults do not list, and still produces only JSON. Keep id stable across versions, because registries and callers store it. Make examples real requests that the skill handles well, since both people and models learn the calling convention from them. Skills are descriptive. They do not define an RPC per skill, and the client still sends ordinary messages.
signatures
A card may carry one or more JWS signatures. Each AgentCardSignature has a required base64url protected header, a required base64url signature, and an optional unprotected header object. The protected header must include alg and kid, should set typ to JOSE, and may include a jku pointing at a JWKS.
The signed payload is the card without its signatures field, canonicalized with the JSON Canonicalization Scheme of RFC 8785. Before canonicalizing, the card must follow protobuf field-presence rules. Required fields are always present. Fields declared optional are present exactly when they were set, even to a default value. Other fields holding default values, such as an empty extensions list, are omitted. Verification repeats those steps and checks the signature. Several signatures may be present to support key rotation.
import base64, json
import jcs # RFC 8785 canonicalization
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
def b64u_dec(s): return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def b64u_enc(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()
def verify_es256(card, keys_by_kid):
"""Returns the kid that verified, or None. Sketch: assumes the served card already
omits default-valued fields per the presence rules; a full verifier strips them
using the proto's field-presence information before canonicalizing."""
body = {k: v for k, v in card.items() if k != "signatures"}
payload = b64u_enc(jcs.canonicalize(body))
for sig in card.get("signatures", []):
header = json.loads(b64u_dec(sig["protected"]))
key = keys_by_kid.get(header.get("kid"))
if header.get("alg") != "ES256" or key is None:
continue
raw = b64u_dec(sig["signature"]) # JWS ES256 is r||s, 32 bytes each
der = encode_dss_signature(int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big"))
try:
key.verify(der, f'{sig["protected"]}.{payload}'.encode(), ec.ECDSA(hashes.SHA256()))
return header["kid"]
except InvalidSignature:
continue
return NoneThe specification requires verifiers to remove default-valued fields before canonicalizing, and only the proto knows which fields are declared optional and must be kept, so the sketch skips that step and assumes a well-formed card. A client that parses the card into typed objects and serializes it again may add or drop default fields and break verification. A signature proves the card came from whoever holds the key. It does not prove that the key belongs to the domain you meant to reach, so pin keys or tie them to the domain through your trust policy. Agent registries show how verification fits into ingestion.
Migrating a 0.3 card
| 0.3 field | 1.0 equivalent |
|---|---|
url with preferredTransport | First entry of supportedInterfaces |
additionalInterfaces | Later entries of supportedInterfaces |
Top-level protocolVersion | protocolVersion on each interface, Major.Minor only |
security | securityRequirements with scopes wrapped in list |
supportsAuthenticatedExtendedCard | capabilities.extendedAgentCard |
capabilities.stateTransitionHistory | No longer part of AgentCapabilities |
Flat scheme with type | Wrapped scheme such as oauth2SecurityScheme |
A lint pass catches most migration errors before a client does.
REQUIRED = ["name", "description", "supportedInterfaces", "version",
"capabilities", "defaultInputModes", "defaultOutputModes", "skills"]
LEGACY_03 = ["url", "preferredTransport", "additionalInterfaces", "protocolVersion",
"security", "supportsAuthenticatedExtendedCard"]
def lint_card(card):
problems = [f"missing {k}" for k in REQUIRED if k not in card]
problems += [f"0.3 field {k} at top level" for k in LEGACY_03 if k in card]
for i, iface in enumerate(card.get("supportedInterfaces", [])):
for k in ("url", "protocolBinding", "protocolVersion"):
if k not in iface:
problems.append(f"interface {i} missing {k}")
if iface.get("protocolVersion", "").count(".") > 1:
problems.append(f"interface {i}: use Major.Minor, not a patch version")
ids = [s.get("id") for s in card.get("skills", [])]
if len(ids) != len(set(ids)):
problems.append("duplicate skill id")
for s in card.get("skills", []):
for k in ("id", "name", "description", "tags"):
if k not in s:
problems.append(f"skill {s.get('id')} missing {k}")
names = set(card.get("securitySchemes", {}))
for scheme in card.get("securitySchemes", {}).values():
if "type" in scheme:
problems.append("flat 0.3 security scheme; 1.0 wraps it, e.g. oauth2SecurityScheme")
for req in card.get("securityRequirements", []):
for name in req.get("schemes", {}):
if name not in names:
problems.append(f"requirement names undefined scheme {name}")
return problems
Failure modes
- Flat security schemes. A 0.3-style scheme in a 1.0 card fails to parse in strict clients, and lenient ones may silently treat the agent as unauthenticated.
- Wrong interface order. Listing a slower or deprecated binding first sends every new client there.
- Patch versions.
1.0.2inprotocolVersionbreaks negotiation, which compares Major.Minor only. - Dropped tenant. Clients that ignore the interface's
tenantget routed to the wrong agent behind a shared endpoint. - Capability drift. Advertising
streamingorpushNotificationsthat a deployment has switched off produces errors that look like transport faults. - Signature breaks after re-serialization. A proxy or CDN that pretty-prints or rewrites the JSON is harmless, because canonicalization removes whitespace and key order. One that adds or removes fields breaks every signature.
- Unchanged version. Editing the card without bumping
versionleaves caches keyed on it serving the old card.
What to do next
- Run the lint script against every card you publish, and fail the deploy on any finding.
- Order
supportedInterfacesby preference, use Major.Minor versions, and keep a 0.3 interface only while old clients need it. - Rewrite security schemes in the wrapped 1.0 form, and express requirements as an OR of ANDs with explicit scopes.
- Give every skill a stable id, specific tags, real examples and only the modes that differ from the defaults.
- Sign the card, publish the key through a JWKS, and verify the served bytes from outside your network.
- Serve the card with
Cache-Controland anETagderived fromversion, and bump the version on every change.