# A2A agent card example: agent-card.json, required fields and signing

> A working A2A v1.0 agent card for /.well-known/agent-card.json, each field marked required or optional, the old agent.json path and how to sign it.

- Source: /learn/a2a-agent-card-example
- Updated: 2026-10-27
- Reading time: 7 minutes
- Tags: A2A, agent card, signing, discovery

---
An A2A agent card is the JSON document an A2A server publishes to say who it is and how to call it. The standard location is `https://{domain}/.well-known/agent-card.json`. Version 1.0.0 of the specification requires 8 top-level fields: `name`, `description`, `supportedInterfaces`, `version`, `capabilities`, `defaultInputModes`, `defaultOutputModes` and `skills`. Version 0.2.6 recommended `/.well-known/agent.json` instead.

## Where does the agent card go: agent-card.json or agent.json?

An A2A server must publish a card, according to section 8.1 of the [specification](https://a2a-protocol.org/latest/specification/). Section 8.2 names 3 ways for a client to find one: the well-known URI, a registry or catalog, and direct configuration. Section 14.3 registers the URI suffix `agent-card.json` with the status Permanent and says the resource MUST return an `AgentCard` object.

The older name was `agent.json`. The [0.2.6 specification](https://a2a-protocol.org/v0.2.6/specification/) recommends `/.well-known/agent.json`. The [0.3.0 specification](https://a2a-protocol.org/v0.3.0/specification/) recommends `/.well-known/agent-card.json`, and so does 1.0.0. The 1.0.0 text does not mention `agent.json`.

The [GitHub releases](https://github.com/a2aproject/A2A/releases) date 0.2.6 to 17 July 2025, 0.3.0 to 30 July 2025 and 1.0.0 to 12 March 2026. A 1.0.1 patch followed on 28 May 2026, and the spec says patch numbers do not affect compatibility. Serve the current card at `agent-card.json`. The scanner reads both paths.

## A2A agent card example

This card follows the 1.0.0 field list in [section 4.4.1](https://a2a-protocol.org/latest/specification/#441-agentcard). It describes an invented order-status agent on example.com, so replace every value.

```json
{
  "name": "Order status agent",
  "description": "Answers questions about the status of orders placed on example.com.",
  "supportedInterfaces": [
    {
      "url": "https://example.com/a2a/v1",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    }
  ],
  "provider": {
    "organization": "Example Shop",
    "url": "https://example.com"
  },
  "version": "1.0.0",
  "documentationUrl": "https://example.com/docs/order-agent",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain", "application/json"],
  "skills": [
    {
      "id": "order-status",
      "name": "Order status",
      "description": "Returns the status and delivery estimate of one order, given an order number.",
      "tags": ["orders", "shipping"],
      "examples": ["Where is order 10432?"]
    }
  ]
}
```

To list the fields that a published card is missing, compare its keys with the required 8. An empty array means none is missing:

```sh
curl -sS https://example.com/.well-known/agent-card.json \
  | jq '["name","description","supportedInterfaces","version","capabilities","defaultInputModes","defaultOutputModes","skills"] - keys'
```

## Which agent card fields are required?

Section 4.4.1 lists 14 top-level fields. Eight are required. The example above uses all 8, plus the optional `provider` and `documentationUrl`.

| Field | Required | What it holds |
|---|---|---|
| `name` | Yes | A human-readable name for the agent |
| `description` | Yes | What the agent is for, for people and other agents |
| `supportedInterfaces` | Yes | Ordered list of interfaces. The first entry is preferred |
| `version` | Yes | The version of the agent, such as `1.0.0` |
| `capabilities` | Yes | The optional features the agent supports |
| `defaultInputModes` | Yes | Media types the agent accepts, across all skills |
| `defaultOutputModes` | Yes | Media types the agent returns |
| `skills` | Yes | What the agent can do, as a list of skill objects |
| `provider` | No | The organization and URL behind the agent |
| `documentationUrl` | No | A link to more documentation |
| `securitySchemes` | No | How to authenticate: API key, HTTP, OAuth 2.0, OpenID Connect or mutual TLS |
| `securityRequirements` | No | Which of those schemes a caller must use |
| `signatures` | No | JSON Web Signatures computed over the card |
| `iconUrl` | No | A link to an icon |

Four of the required fields are arrays, and section 5.7 says a required array MUST hold at least one element. The nested objects have their own required members:

- **Each `supportedInterfaces` entry:** `url`, `protocolBinding` and `protocolVersion` are required. `tenant` is optional. The core `protocolBinding` values are `JSONRPC`, `GRPC` and `HTTP+JSON`.
- **Each `skills` entry:** `id`, `name`, `description` and `tags` are required. `examples`, `inputModes`, `outputModes` and `securityRequirements` are optional.
- **`provider`:** `organization` and `url` are both required once `provider` is present.
- **`capabilities`:** the field is required, and every member is optional. They are `streaming`, `pushNotifications`, `extensions` and `extendedAgentCard`.

The spec names `a2a.proto` as the single normative definition of these objects. A generated JSON file is non-normative, and the spec says it is not committed. There is no JSON Schema to validate against, so the tables of section 4 are what to read.

The card is public. Section 14.3 says it SHOULD NOT hold credentials or internal detail. A card that sets `capabilities.extendedAgentCard` to `true` offers a second card, with more detail, to clients that authenticate.

## What changed between v0.3 and v1.0 cards?

The [what's new page](https://a2a-protocol.org/latest/whats-new-v1/) lists 3 changes to the card's fields. `url`, `preferredTransport` and `additionalInterfaces` became one list, `supportedInterfaces`. `protocolVersion` moved from the card into each interface. `supportsAuthenticatedExtendedCard` became `capabilities.extendedAgentCard`.

A v0.3 card has a top-level `url` and no `supportedInterfaces`. The scanner takes it for an agent, because it accepts a `url`. It then reads the card as Served with an error, because it finds no transport. Moving `url` and `protocolVersion` into a `supportedInterfaces` entry fixes it.

## How do you sign an agent card?

Signing is optional. Section 8.4 says a card MAY carry a JSON Web Signature, and section 14.3 says a client SHOULD verify one when it is present. The procedure of section 8.4.2 has 5 steps:

1. Take the card without its `signatures` member. The spec also removes members that hold default values, such as an empty array, so leave those out of the card.
2. Canonicalize the rest with RFC 8785.
3. Write a protected header with `alg`, `typ` and `kid`, and optionally `jku`. The spec says `typ` SHOULD be `JOSE`.
4. Sign `base64url(header)`, a dot and `base64url(canonical card)`.
5. Put the base64url header in `protected` and the base64url signature in `signature`, inside a `signatures` array.

This script does all 5 with Node's built-in `crypto`. It makes a throwaway key to show the shape. In production, load your stored private key and publish its public half under the same `kid`.

```js
// node sign-card.mjs agent-card.json
import { readFileSync, writeFileSync } from 'node:fs';
import { generateKeyPairSync, sign } from 'node:crypto';

// RFC 8785 canonical JSON, enough for strings, booleans, integers, arrays and objects.
const canonical = (v) =>
  v === null || typeof v !== 'object' ? JSON.stringify(v)
  : Array.isArray(v) ? `[${v.map(canonical).join(',')}]`
  : `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;

const { signatures, ...card } = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });

const header = { alg: 'ES256', typ: 'JOSE', kid: 'key-1', jku: 'https://example.com/.well-known/jwks.json' };
const protectedValue = Buffer.from(JSON.stringify(header)).toString('base64url');
const input = `${protectedValue}.${Buffer.from(canonical(card)).toString('base64url')}`;
const signature = sign('sha256', Buffer.from(input), { key: privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64url');

writeFileSync('agent-card.signed.json', JSON.stringify({ ...card, signatures: [{ protected: protectedValue, signature }] }, null, 2));
writeFileSync('jwks.json', JSON.stringify({ keys: [{ ...publicKey.export({ format: 'jwk' }), kid: 'key-1', use: 'sig', alg: 'ES256' }] }, null, 2));
```

Serve `agent-card.signed.json` as the card and `jwks.json` at the `jku` address. Sign again after every edit, because a changed `version` no longer matches the signature. To read the header back out of a signed card:

```sh
jq -r '.signatures[0].protected' agent-card.signed.json \
  | python3 -c "import sys,base64,json;p=sys.stdin.read().strip();p+='='*(-len(p)%4);print(json.dumps(json.loads(base64.urlsafe_b64decode(p)),indent=2))"
```

```json
{
  "alg": "ES256",
  "typ": "JOSE",
  "kid": "key-1",
  "jku": "https://example.com/.well-known/jwks.json"
}
```

A client then does the reverse, as section 8.4.3 describes. It finds the key by `kid` and `jku`, or in a trusted key store. It removes default-valued members and `signatures`, canonicalizes the card and checks the signature.

## How does AgenticWebScore check an agent card?

The [A2A agent card check](/checks/a2a-card) is an unscored agent convention marked draft. It is asked only of a site where the scan saw an A2A agent. That means an `agent-card.json` with a `name` and either `supportedInterfaces` or `url`, or an AI catalog entry for an agent card. A card that exists only at `agent.json` does not trigger the check.

Once asked, the check reads `/.well-known/agent-card.json` and then `/.well-known/agent.json`. It uses the first that holds a JSON object.

- **Present.** The card has `name`, `description`, `version`, a `capabilities` object, at least 1 input mode, 1 output mode and 1 skill, and at least 1 `supportedInterfaces` entry with `url`, `protocolBinding` and `protocolVersion`.
- **Served with an error.** The card has a name, description, version and skills but lacks a required field such as a usable interface, or the JSON is not an agent card.
- **Not used.** Both paths answered, and neither served a JSON card.
- **Not measured.** A path was not requested, did not answer, or answered 406 to the scan's `Accept` header.

The check counts skills and does not read each skill's `id`, `name`, `description` or `tags`. It reads the card and does not call the endpoint in `supportedInterfaces`.

The [signed agent card check](/checks/a2a-signed) is also unscored. It is out of scope when no card is published.

- **Present.** A signature verifies. The protected header names `alg`, `typ`, `kid` and `jku`, and the key set at `jku` is on the card's own host and publishes that `kid`.
- **Not used.** The card carries no `signatures`, because signing is optional.
- **Served with an error.** A signature does not verify, or the key set has no such `kid`.
- **Not measured.** The key set is on another host, was not requested or did not answer.

This check differs from the spec in 2 places:

- The spec marks `jku` optional and allows a trusted key store. A card signed that way reads Served with an error here.
- The spec removes default-valued members before canonicalizing, in section 8.4.3. This check removes only `signatures`. A card that follows the spec's own example and still carries `"extensions": []` therefore reads Served with an error. Leave empty arrays out of the card.

On 20 October 2026 the scan found no A2A agent on cloudflare.com, github.com, resend.com or vercel.com. MDN was not asked, because the scan found no API or agent surface there. This site's record of 25 October found none either. Both checks were out of scope on every measured site. The example above is built from the spec's field list and not copied from a live card.

To point an ARD manifest at a card, add an entry whose `type` is `application/a2a-agent-card+json` and whose `url` is the card's address. The entry format is in [ard.json and the ai-catalog.json rename](/learn/ard-json-agentic-resource-discovery). To see whether the scan finds an agent card on your site, [scan it](/scan).
