How to create an agent.json file for A2A v1.0

11 min read
Client agentyour-agent.comGET /.well-known/agent-card.json200 · application/a2a+json{ name, description, version, capabilities, supportedInterfaces[], skills[] }
Discovery is one unauthenticated GET against a fixed path. No registry, no handshake, no SDK.

An Agent Card is a small JSON file with an oversized job. It is the only thing another agent reads before deciding whether to talk to yours: what you do, where you listen, what you speak, and how to authenticate. Get it wrong and you are not broken — you are invisible, which is harder to debug.

What the file actually is

The Agent Card is a static JSON document served over HTTPS at a fixed path on your domain:

https://your-agent.com/.well-known/agent-card.json

That path is not decorative. /.well-known/ is a reserved prefix defined by RFC 8615 for exactly this purpose — metadata a client can fetch without being told where to look. Because the location is fixed, discovery needs no registry, no SDK and no credential: a client that knows your domain knows your card's URL.

A2A v1.0 uses agent-card.json. The pre-1.0 path was agent.json, which is why the file is still widely called "agent.json" and why you should serve both.

The eight fields you cannot skip

The A2A specification requires eight top-level fields. Everything else — provider, iconUrl, documentationUrl, securitySchemes, signatures — is optional, though several are effectively mandatory in production for reasons covered below.

FieldTypeWhat it answers
namestringWhich agent is this?
descriptionstringWhat does it do, and when should I use it?
versionstringWhich build of the agent — not the protocol
supportedInterfacesAgentInterface[]Where do I send requests, and in what protocol?
capabilitiesAgentCapabilitiesCan it stream? Can it call me back?
defaultInputModesstring[]What media types can I send?
defaultOutputModesstring[]What will I get back?
skillsAgentSkill[]What specific jobs will it take?

Writing the card, field by field

Here is a complete, valid card. Every line after this section is commentary on it.

/.well-known/agent-card.json
{
  "name": "Invoice Reconciliation Agent",
  "description": "Matches incoming payments against open invoices, flags
                  partial and duplicate payments, and drafts customer
                  follow-ups for anything it cannot reconcile.",
  "version": "3.1.0",
  "provider": {
    "organization": "Northwind Finance",
    "url": "https://northwind.example.com"
  },
  "documentationUrl": "https://docs.northwind.example.com/agents/reconciliation",
  "iconUrl": "https://northwind.example.com/icons/reconciliation.png",
  "supportedInterfaces": [
    {
      "url": "https://agents.northwind.example.com/reconciliation/a2a/v1",
      "protocolBinding": "JSONRPC",
      "protocolVersion": "1.0"
    },
    {
      "url": "https://agents.northwind.example.com/reconciliation/grpc",
      "protocolBinding": "GRPC",
      "protocolVersion": "1.0"
    }
  ],
  "capabilities": {
    "streaming": true,
    "pushNotifications": true,
    "extendedAgentCard": false
  },
  "defaultInputModes": ["application/json", "text/plain"],
  "defaultOutputModes": ["application/json"],
  "securitySchemes": {
    "corporate-sso": {
      "openIdConnectSecurityScheme": {
        "openIdConnectUrl": "https://sso.northwind.example.com/.well-known/openid-configuration"
      }
    }
  },
  "security": [{ "corporate-sso": ["openid", "agents.invoke"] }],
  "skills": [
    {
      "id": "reconcile-payment-batch",
      "name": "Reconcile a payment batch",
      "description": "Given a bank statement export, match each line to an
                      open invoice and report exceptions with reasons.",
      "tags": ["finance", "invoices", "reconciliation"],
      "examples": [
        "Reconcile yesterday's ACH batch against open invoices.",
        "Why was payment 8823 not matched to an invoice?"
      ]
    }
  ]
}

name and description

These are read by another agent's model, not by a human browsing a catalogue. Write the description as a routing decision aid: say what the agent does and, crucially, where its competence ends. "Handles invoices" tells a caller nothing it can act on. "Matches incoming payments against open invoices and flags exceptions; does not issue refunds or modify ledgers" lets a caller rule the agent in or out in one pass.

supportedInterfaces

This array replaced four separate v0.x fields, and it is ordered: the first entry is the preferred one. Each entry needs all three of url, protocolBinding and protocolVersion. The core bindings are JSONRPC, GRPC and HTTP+JSON; anything custom should be identified by a URI so it cannot collide with a future core binding.

List an interface only if it is actually reachable at that URL right now. A card advertising a gRPC endpoint you have not deployed produces the worst kind of failure — a caller that picks it, fails, and does not retry with your JSON-RPC endpoint.

capabilities

Three booleans that change how a caller structures the whole interaction. streaming tells it whether to expect incremental results. pushNotifications tells it whether your agent can call back when a long task finishes, or whether it must poll. extendedAgentCard tells it whether an authenticated fetch returns a richer card than the public one.

Declare these honestly. They are promises, and unlike prose, callers act on them automatically.

defaultInputModes and defaultOutputModes

IANA media types, not friendly names: text/plain, application/json, image/png, application/pdf. If your agent accepts free-form instructions, it accepts text/plain — say so, because a caller with only a natural-language request needs to know it will be understood.

Skills: the section that gets cards rejected

Every entry in skills[] needs four things: id, name, description and tags. That last one is the one people miss — tags became required in v1.0, because they are what skill-level discovery filters on. A skill without tags is a skill nothing can search for.

One skill, complete
{
  "id": "reconcile-payment-batch",
  "name": "Reconcile a payment batch",
  "description": "Given a bank statement export, match each line to an open
                  invoice and report exceptions with reasons.",
  "tags": ["finance", "invoices", "reconciliation"],
  "examples": [
    "Reconcile yesterday's ACH batch against open invoices.",
    "Why was payment 8823 not matched to an invoice?"
  ],
  "inputModes": ["text/csv", "application/json"],
  "outputModes": ["application/json"]
}
  • id is a stable machine identifier. Keep it kebab-case and never reuse it for a different behaviour — callers may pin to it.
  • tags should be the words someone searching for this capability would use, not your internal vocabulary. Three to six is plenty.
  • examples is optional and disproportionately valuable: two or three real request phrasings do more to make your agent correctly invoked than another paragraph of description.
  • inputModes/outputModes override the card-level defaults for that one skill. Use them when a single skill takes something unusual — a CSV upload, say — rather than widening the defaults for the whole agent.

Declaring authentication without leaking it

The card is fetched without credentials, so it must explain how to get in without containing the key. Two fields do this: securitySchemes defines the mechanisms, security lists which of them a caller must satisfy.

API key in a header
"securitySchemes": {
  "service-key": {
    "apiKeySecurityScheme": {
      "location": "header",
      "name": "X-API-Key",
      "description": "Issued from the Northwind partner console."
    }
  }
},
"security": [{ "service-key": [] }]
OAuth 2.0 client credentials
"securitySchemes": {
  "partner-oauth": {
    "oauth2SecurityScheme": {
      "flows": {
        "clientCredentials": {
          "tokenUrl": "https://sso.northwind.example.com/oauth2/token",
          "scopes": { "agents.invoke": "Send tasks to Northwind agents" }
        }
      }
    }
  }
},
"security": [{ "partner-oauth": ["agents.invoke"] }]

Serving it at the right URL

Writing valid JSON is the easy half. Most cards that fail in the wild are correct documents served wrongly. Four things have to be true: the path is exact, the scheme is HTTPS, the content type is application/a2a+json, and cross-origin reads are allowed.

Next.js App Router — app/.well-known/agent-card.json/route.ts
import { agentCard } from '@/config/agentCard'

export function GET() {
  return Response.json(agentCard, {
    headers: {
      'Content-Type': 'application/a2a+json',
      'Cache-Control': 'public, max-age=3600, s-maxage=86400',
      'Access-Control-Allow-Origin': '*'
    }
  })
}
Express
import agentCard from './agent-card.json' with { type: 'json' }

app.get('/.well-known/agent-card.json', (_req, res) => {
  res.type('application/a2a+json')
  res.set('Cache-Control', 'public, max-age=3600')
  res.set('Access-Control-Allow-Origin', '*')
  res.json(agentCard)
})

// Pre-1.0 clients look here first.
app.get('/.well-known/agent.json', (_req, res) =>
  res.redirect(308, '/.well-known/agent-card.json')
)
nginx, serving a static file
location = /.well-known/agent-card.json {
    alias /var/www/agent/agent-card.json;
    types { } default_type "application/a2a+json";
    add_header Cache-Control "public, max-age=3600";
    add_header Access-Control-Allow-Origin "*";
}

location = /.well-known/agent.json {
    return 308 /.well-known/agent-card.json;
}

Serve it from the same origin the agent is associated with, at the root — not /api/.well-known/agent-card.json, not a path under a subdirectory. If your agent lives at agents.example.com, the card belongs at agents.example.com/.well-known/agent-card.json, even if your marketing site is example.com.

Verifying it from the outside

Validate the document and the delivery separately, because they fail for different reasons. Start with curl, which shows you both at once:

curl -sSI https://your-agent.com/.well-known/agent-card.json
# HTTP/2 200
# content-type: application/a2a+json
# cache-control: public, max-age=3600

curl -s https://your-agent.com/.well-known/agent-card.json | jq .

Then check the three things curl cannot tell you:

  1. Is the JSON actually a valid v1.0 card? Paste it into the Agent Card validator, which checks required fields, interface shape, skill tags and pre-1.0 leftovers, and flags anything that looks like an embedded credential.
  2. Are both discovery paths live? The well-known URL checker fetches /.well-known/agent-card.json and the legacy /.well-known/agent.json from your real domain and reports the status code and content type of each.
  3. Does it look right to a stranger? Read your own description as though you had never seen the agent. If you cannot tell from the card alone when to route work to it, neither can anyone else's model.

For a live example to compare against, this site serves its own card at /.well-known/agent-card.json.

The failures that stay silent

None of these produce an error anywhere. They just mean nothing finds you, or finds you and gives up.

SymptomUsual causeFix
Card validates, nothing discovers itServed only at /.well-known/agent.jsonServe agent-card.json as the canonical path and alias the legacy one
Browser-based clients see nothingNo CORS header on the responseAdd Access-Control-Allow-Origin: *
Checker reports the wrong content typeStatic hosting defaulting to application/jsonOverride the media type to application/a2a+json
Callers pick an endpoint that failsAn aspirational entry in supportedInterfacesList only interfaces that are deployed; the first entry is the preferred one
Skills never matchedMissing or internal-jargon tagsTag with the words a searcher would use — tags are required in v1.0
Card goes stale after a deployHand-maintained JSON checked into the repoGenerate it from the same config that configures the server

If you would rather not assemble the JSON by hand, the Agent Card generator produces a v1.0-shaped document from a short form, and the schema reference documents every field, including the optional ones this walkthrough skipped.

Frequently asked questions

Is the file called agent.json or agent-card.json?

A2A v1.0 standardises on /.well-known/agent-card.json. /.well-known/agent.json is the pre-1.0 path and is still worth serving as an alias, because clients written against older SDKs look there first. Both should return the same document; serving one and 404ing the other is the most common reason a card that validates locally is undiscoverable in practice.

Do I need a separate Agent Card per skill?

No. One agent means one card, and the skills[] array carries as many skills as that agent offers. Split into separate cards only when the things behind them are genuinely separate agents — different endpoints, different lifecycles, different owners.

Can the Agent Card be generated at request time?

Yes, and it often should be. Serving it from a route handler lets you fill the endpoint URL from configuration so staging and production advertise themselves correctly, and lets version track your deployed build. Just keep the response cacheable — discovery clients will fetch it far more often than it changes.

Does the card have to be publicly accessible?

The public card does, and it should be fetchable without credentials — that is what makes discovery work. If parts of your agent are sensitive, keep them out of the public card and declare capabilities.extendedAgentCard, which signals that an authenticated client can retrieve a fuller version.

What content type should the response use?

application/a2a+json. Plain application/json will still parse in most clients, but the specific media type is what lets a checker confirm it reached an Agent Card endpoint rather than an API that happens to return JSON. It costs one header.

References

  1. A2A protocol specification (a2aproject/A2A)github.com/a2aproject/A2A
  2. RFC 8615 — Well-Known Uniform Resource Identifierswww.rfc-editor.org/rfc/rfc8615
  3. IANA media types registrywww.iana.org/assignments/media-types/media-types.xhtml
  4. MDN — Cross-Origin Resource Sharing (CORS)developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Related tool: Agent Card Generator

Next step

Fill in a form, get a valid card.

The generator writes an A2A v1.0 Agent Card from your endpoint, provider and skill details — correct field names, correct nesting, ready to serve.