AIScan guide cover: Publish your A2A card at agent-card.json, with Next.js, Astro and Cloudflare logos and three steps: write the card, serve it, validate it
AIScan guide cover: Publish your A2A card at agent-card.json, with Next.js, Astro and Cloudflare logos and three steps: write the card, serve it, validate it
AI Readiness

Publish an A2A Agent Card at /.well-known/agent-card.json, Tested on Every Stack

Where an A2A Agent Card goes (agent-card.json, not agent.json), the 8 required fields, and setup for Next.js, Astro, Cloudflare, Netlify, Vercel and WordPress.

12 Nov 2026 11 min read
#A2A#agent card#well-known#agent discovery#AI agents

This guide covers P2 · Capabilities — for Next.js / Vercel.

Table of contents

An A2A Agent Card belongs at /.well-known/agent-card.json, served as application/json, with eight required top-level fields filled in. The older path, /.well-known/agent.json, came from A2A 0.2.x and was renamed in 0.3.0, so a client written for the current spec will not look there. This guide shows how to publish the card on a static host, Next.js, Astro, Cloudflare, Netlify, Vercel and WordPress, and how to prove it parses with the official Python SDK before any agent tries it.

By M Asif Rahman. Updated 12 November 2026. Every result below comes from builds and scripts we ran on 12 November 2026 with a2a-sdk 1.3.0, Next.js 16.4.6 and Astro 7.4.11.

Quick summary

QuestionShort answerSource
Which path?/.well-known/agent-card.jsonA2A spec 1.0.0, §8.2 and §14.3
Is agent.json still valid?Only in the 0.2.x spec. 0.3.0 and 1.0.0 use agent-card.jsona2a-protocol.org, versions 0.2.6, 0.3.0, 1.0.0
Content typeapplication/jsonspec §14.3 example, our Next.js and Astro builds
Required card fieldsname, description, version, supportedInterfaces, capabilities, defaultInputModes, defaultOutputModes, skillsREQUIRED field behaviour in a2a-sdk 1.3.0
Quickest validatorthe 40-line script below, built on the SDK's own parserour lab, 8 test cards
Does AIScan score it?No. AIScan checks the MCP Server Card (P2) and its neighbours, not A2A cardsAIScan check list, 12 Nov 2026
Who does check it?Cloudflare's Is It Agent Ready, check a2aAgentCardits scan evidence for aiscan.site, 12 Nov 2026

If you only have five minutes: copy the minimal card below, put it in your site's public/.well-known/ folder, deploy, and run the validator against the live URL.

Why two paths exist

A2A (Agent2Agent) is the open protocol for one AI agent to find and call another. The Agent Card is its business card: who the agent is, which URL and transport to use, what auth it wants and what skills it offers. A client that only knows your domain finds the card by fetching a fixed URL, the same well-known URI pattern RFC 8615 defines for security.txt and friends.

The fixed URL changed once. Each version below was fetched from a2a-protocol.org on 12 November 2026, and we counted the discovery path it names:

Spec versionDiscovery path it namesCard shape
0.2.6/.well-known/agent.jsontop-level url, protocolVersion
0.3.0/.well-known/agent-card.jsontop-level url, preferredTransport
1.0.0 (latest)/.well-known/agent-card.jsonsupportedInterfaces list

According to the current specification, §14.3 registers the suffix agent-card.json with status Permanent, and the IANA Well-Known URIs registry lists it. The registry has no entry for agent.json. The Python SDK hard-codes the new path too: AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent-card.json' in a2a/utils/constants.py.

That is why tutorials disagree. Anything written in mid-2025 against 0.2.x says agent.json, and plenty of those posts still rank. Our llms.txt vs robots.txt vs sitemap guide already notes the registered name. This post is the setup half.

What a valid card needs

According to the field options in a2a-sdk 1.3.0's protobuf model, these fields are REQUIRED. We read them from the installed package, not from a blog post:

ObjectRequired fields
AgentCardname, description, version, supportedInterfaces, capabilities, defaultInputModes, defaultOutputModes, skills
AgentInterface (each entry)url, protocolBinding, protocolVersion
AgentSkill (each entry)id, name, description, tags
AgentProvider (if present)organization, url

A minimal card that passes:

{
  "name": "Example Support Agent",
  "description": "Answers order-status questions for example.com customers.",
  "version": "1.0.0",
  "supportedInterfaces": [
    {"url": "https://example.com/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0"}
  ],
  "capabilities": {"streaming": false},
  "defaultInputModes": ["text/plain"],
  "defaultOutputModes": ["text/plain"],
  "skills": [
    {"id": "order-status", "name": "Order status", "description": "Looks up an order by number.", "tags": ["orders"]}
  ]
}

version is your agent's version, a string. protocolVersion lives on each interface and names the A2A version that endpoint speaks. According to the spec's §14.3 security notes, the card should not include sensitive credentials or internal implementation details, and HTTPS should carry it. Keep API keys out of it; the securitySchemes field describes how to authenticate, it never holds the secret.

Check it: the scan first, then the validator

Run the AIScan scan first (npx aiscan-cli yoursite.com, or paste the URL at aiscan.site, free, no account). It will not grade the A2A card, and we would rather say so than imply it does. What it does grade is the discovery layer the card sits in: P1 (API catalog), P2 (MCP Server Card), P3 (Agent Skills index) and P4 (OAuth discovery), all listed on the capabilities checks page. If P2 already passes, your host serves /.well-known/ correctly and the card will be reachable at the same prefix.

For the card itself, Cloudflare's Is It Agent Ready runs a check called a2aAgentCard. Its evidence for aiscan.site on 12 November 2026 showed exactly one request, GET /.well-known/agent-card.json, then a conclusion. It did not try agent.json.

If you would rather check by hand, this script wraps the SDK's own parse_agent_card() and then walks every REQUIRED field:

#!/usr/bin/env python3
# pip install a2a-sdk==1.3.0   then: python validate_card.py https://example.com/.well-known/agent-card.json
import json, sys, urllib.request
from a2a.client.card_resolver import parse_agent_card
from google.api import field_behavior_pb2 as fb

def load(src):
    if src.startswith('http'):
        req = urllib.request.Request(src, headers={'User-Agent': 'card-check/1.0'})
        with urllib.request.urlopen(req, timeout=15) as r:
            print('HTTP', r.status, r.headers.get('Content-Type'))
            return json.load(r)
    return json.load(open(src))

def missing(msg, path):
    out = []
    for f in msg.DESCRIPTOR.fields:
        name = f'{path}.{f.json_name}'
        required = fb.REQUIRED in f.GetOptions().Extensions[fb.field_behavior]
        val = getattr(msg, f.name)
        if f.is_repeated:
            if required and len(val) == 0: out.append(name)
            if f.message_type and not f.message_type.GetOptions().map_entry:
                for i, item in enumerate(val): out += missing(item, f'{name}[{i}]')
        elif f.message_type:
            if msg.HasField(f.name): out += missing(val, name)
            elif required: out.append(name)
        elif required and val in ('', 0):
            out.append(name)
    return out

raw = load(sys.argv[1])
legacy = [k for k in ('url', 'protocolVersion', 'preferredTransport', 'additionalInterfaces') if k in raw]
try:
    card = parse_agent_card(dict(raw))
except Exception as e:
    print('FAIL parse:', e); sys.exit(1)
gaps = missing(card, 'card')
if legacy: print('NOTE legacy 0.x fields mapped by the SDK:', ', '.join(legacy))
for i in card.supported_interfaces:
    print(f'interface {i.protocol_binding} {i.protocol_version} {i.url}')
print('missing required:', ', '.join(gaps) if gaps else 'none')
print('RESULT:', 'FAIL' if gaps else 'PASS')
sys.exit(1 if gaps else 0)

We ran it on eight cards. The surprise is in rows two and three:

Test cardResultWhat the validator printed
Minimal 1.0 card abovePASSinterface JSONRPC 1.0 https://example.com/a2a/v1
Old 0.2.x card (top-level url, protocolVersion: "0.2.6")PASSlegacy fields mapped into one interface at 0.2.6
1.0 card that also keeps an old top-level urlPASSthe old url is dropped without a warning
No supportedInterfaces, no urlFAILcard.supportedInterfaces
No skillsFAILcard.skills
No capabilitiesFAILcard.capabilities
Skill without tagsFAILcard.skills[0].tags
version as a numberFAILFailed to parse version field

So the SDK is forgiving about old cards. It does not reject a 0.2.x card. It converts it, keeping protocolVersion at 0.2.6, which tells a 1.0 client the endpoint speaks the old protocol. And when a card has both shapes, the SDK keeps supportedInterfaces and quietly discards the top-level url. If those two URLs differ, the one you edited last may be the one nobody calls.

Path 1: a static file on any host

The simplest route works on GitHub Pages, Netlify, Vercel, Cloudflare and any plain web server.

  1. Save the card as agent-card.json.
  2. Put it in a folder named .well-known inside the folder your host publishes: public/ for most frameworks, the web root for a plain server.
  3. Deploy.
  4. Run curl -sI https://yoursite.com/.well-known/agent-card.json. You want 200 and a JSON content type.
  5. Run the validator against the live URL.

A 404 or 403 at step 4 usually means a build or server rule is hiding dot-folders. Our /.well-known/ 404 fix covers Jekyll on GitHub Pages, the nginx dotfile deny rule and Apache, each tested.

Path 2: Next.js

A static file in public/.well-known/ works as is. Our Next.js 16.4.6 production build served public/.well-known/static-card.json with Content-Type: application/json; charset=UTF-8 and an ETag. Use a route handler when you want your own headers.

  1. Create app/.well-known/agent-card.json/route.js. The folder name really is agent-card.json.
  2. Put the card in app/.well-known/card.json and paste:
import card from '../card.json';
export const dynamic = 'force-static';
export function GET() {
  return Response.json(card, {
    headers: { 'Cache-Control': 'public, max-age=3600', 'Access-Control-Allow-Origin': '*' },
  });
}
  1. Optional, for old clients: app/.well-known/agent.json/route.js with return Response.redirect(new URL('/.well-known/agent-card.json', request.url), 301);.
  2. Run npm run build. The route list should show ○ /.well-known/agent-card.json (static).

What next start returned on 12 November 2026:

/.well-known/agent-card.json  HTTP/1.1 200 OK
  content-type: application/json
  cache-control: public, max-age=3600
  access-control-allow-origin: *
/.well-known/agent.json       HTTP/1.1 301 Moved Permanently
  location: http://localhost:3101/.well-known/agent-card.json

The validator passed against the live route. The Next.js route handler docs cover the API. More Next.js setup is on the Next.js platform page.

Path 3: Astro

  1. Put agent-card.json in public/.well-known/. Astro 7.4.11 copied it byte for byte to dist/.well-known/.
  2. For a second path, add an endpoint at src/pages/.well-known/agent.json.ts:
import card from '../../../public/.well-known/agent-card.json';
export function GET() {
  return new Response(JSON.stringify(card), { headers: { 'Content-Type': 'application/json' } });
}
  1. Run npm run build. The log listed /.well-known/agent.json as a prerendered route, and both files sat in dist/.well-known/.
  2. In astro preview, both paths returned 200, Content-Type: application/json and an ETag.

Production headers come from your host, not from Astro, so repeat the curl -sI check after deploy. The Astro endpoints guide explains static endpoints, and the AI readiness setup guide's Astro section collects the rest.

Path 4: Cloudflare, Netlify and Vercel headers

The file goes in the publish folder as in path 1. These snippets add caching and CORS. We did not deploy them live for this post, so verify each with curl -sI after your deploy.

Cloudflare (Workers static assets or Pages): a plain-text _headers file in the static assets folder. According to Cloudflare's headers docs, rules in _headers override what Cloudflare would otherwise send.

/.well-known/agent-card.json
  Content-Type: application/json
  Cache-Control: public, max-age=3600
  Access-Control-Allow-Origin: *

Netlify: the same _headers format in the publish directory, according to Netlify's headers docs. A redirect for old clients goes in _redirects: /.well-known/agent.json /.well-known/agent-card.json 301.

Vercel: in vercel.json. Vercel's permanent: true sends a 308, according to its configuration reference.

{
  "headers": [{"source": "/.well-known/agent-card.json",
    "headers": [{"key": "Cache-Control", "value": "public, max-age=3600"},
                {"key": "Access-Control-Allow-Origin", "value": "*"}]}],
  "redirects": [{"source": "/.well-known/agent.json",
    "destination": "/.well-known/agent-card.json", "permanent": true}]
}

Path 5: WordPress

WordPress does not need a plugin for this. Its standard rewrite rules send a request to index.php only when no real file matches, so a file you upload is served directly. The WordPress .htaccess reference shows the RewriteCond %{REQUEST_FILENAME} !-f line that makes this work.

  1. Open your host's file manager or connect over SFTP.
  2. In the folder that holds wp-config.php, create .well-known if it does not exist. Many hosts already have one for SSL validation.
  3. Upload agent-card.json into it.
  4. Run curl -sI https://yoursite.com/.well-known/agent-card.json. A 403 means an nginx dotfile rule; the fix is in the 404 guide linked in path 1.

Your agent itself still runs elsewhere. The card only points at it.

Which path should you serve?

Serve agent-card.json as the real file. That is what the spec names, what the SDK fetches, and the only path Cloudflare's checker tried.

For agent.json, our judgement: add it only if you published a card there before, or your logs show requests for it. Then pick one form, and know what each costs.

We pointed the SDK's own A2ACardResolver at the Next.js build above, using a default httpx.AsyncClient() (httpx 0.28.1), which is also what the SDK's ClientFactory creates when you pass no client:

RequestServer answerSDK result
/.well-known/agent-card.json200, JSONcard parsed, name read
/.well-known/agent.json301 to agent-card.jsonAgentCardResolutionError ... (HTTP 301)

So a redirect keeps one source of truth but does nothing for a client like that one, because httpx follows redirects only when the client is created with follow_redirects=True. A second copy of the same bytes, as in the Astro example, works for every client but has to be kept in sync with the first. If you want old clients served, the copy is the safer of the two. If you only want tidy logs, the redirect is fine.

According to the spec's §8.6, servers should send Cache-Control with a max-age and an ETag derived from the card's version or a hash of its content, and clients should revalidate with If-None-Match. A card changes rarely, so an hour or a day of max-age is reasonable. Static hosts usually add the ETag for you. The Next.js route handler above did not, so add one there if caching matters to you.

Mistakes we saw in the lab

  • A 404 page with text/html. Our four sites (aiscan.site, xspeedcache.com, storeseo.com, bettershield.ai) all returned 404 text/html for both paths on 12 November 2026, because none runs an A2A agent. That is correct for a site with no agent. Do not publish a card for an agent that does not exist.
  • Two shapes in one card. Remove the top-level url and protocolVersion when you add supportedInterfaces.
  • Empty skills. A card with no skills fails. If the agent does one thing, write one skill.
  • Numbers as strings, or strings as numbers. version must be a string.

Next step

Run the AIScan scan on your domain and read P1 to P4 under capabilities: if they pass, your well-known folder works. Then validate the card with the script above. For the neighbouring files, see publish an MCP Server Card, publish an Agent Skills index, and the agentic commerce protocols explainer for where A2A sits next to UCP, ACP and AP2. Every setup guide is on the guides page.

Frequently asked questions

Should my A2A Agent Card be at agent.json or agent-card.json?

Use /.well-known/agent-card.json. The A2A spec named agent.json in version 0.2.x and renamed it to agent-card.json in 0.3.0; version 1.0.0 and the IANA Well-Known URIs registry both use agent-card.json, and a2a-sdk 1.3.0 fetches only that path by default.

My agent card returns 404 after deploy. What is wrong?

Most often the build or the server hid the dot-folder. Jekyll on GitHub Pages skips folders starting with a dot unless you list them under include, and an nginx rule like location ~ /. { deny all; } blocks them. Check that the file is in your built output, then see our /.well-known/ 404 fix guide.

The validator says missing required: card.skills. Why does my card fail?

The A2A 1.0 AgentCard model marks skills as required, and our validator treats an empty list as missing. Add at least one skill with id, name, description and tags, even if the agent does only one thing.

The SDK raised AgentCardResolutionError with HTTP 301. How do I fix it?

The client asked for a path that redirects, and a default httpx.AsyncClient does not follow redirects. Point the client at /.well-known/agent-card.json directly, create the httpx client with follow_redirects=True, or serve a copy of the card at the old path instead of a redirect.

My 0.2.x card still passes the validator. Is it fine to leave it?

It parses, but it is not current. a2a-sdk 1.3.0 converts a top-level url and protocolVersion into one supportedInterfaces entry and keeps the old protocol version, so 1.0 clients see an endpoint that speaks 0.2.6. Rewrite it with supportedInterfaces and protocolVersion 1.0 if your server speaks 1.0.

Does AIScan score the A2A Agent Card?

No. AIScan grades the discovery files around it, P1 API catalog, P2 MCP Server Card, P3 Agent Skills index and P4 OAuth discovery, which tell you whether your /.well-known/ folder is reachable. Cloudflare's Is It Agent Ready runs an a2aAgentCard check if you want the card itself tested.

What content type should the agent card use?

application/json. Our Next.js 16.4.6 route handler and Astro 7.4.11 preview both returned application/json, and Next.js static files added charset=UTF-8. If curl -sI shows text/html, you are almost certainly looking at a 404 page, not your card.

Do I need an agent card if my site has no AI agent?

No. The card describes an A2A agent you run, with the URL where it accepts requests. A content site with no agent should return 404 at this path, which is what our four sites do; publishing a card for an agent that does not exist sends clients to a dead endpoint.

M Asif Rahman

About the author

Entrepreneur, curious about AI

I have been building companies for a long time, and lately most of my curiosity goes to AI. AIScan is a small personal project I built on a flight from Dubai to Narita. I use it to understand how ready the web is for AI agents, and I share what the scans teach me.

Related guides

Browse every guide by check and platform