All guidesAI visibility and the agentic web

How to Build an A2A Agent Endpoint

The Agent-to-Agent (A2A) protocol lets AI agents discover and communicate with your website's agent directly. By publishing an AgentCard at .well-known/agent.json and implementing a JSON-RPC 2.0 endpoint at /api/agent, you enable any A2A-compatible agent to send tasks to your site and receive structured responses.

By Paul Gosnell Updated March 2026 19 min read

p0stman builds for the agentic web. AgentReady scans your site for llms.txt, structured data and crawler access in under a minute, free.

The web is entering a new era where AI agents do not just crawl and index content -- they communicate with each other. Google introduced the Agent-to-Agent (A2A) protocol in April 2025, and it was quickly adopted by the Linux Foundation as an open governance standard. The protocol defines how independent AI agents discover each other, negotiate capabilities, and exchange tasks without any human intermediary.

For website owners and developers, A2A represents a fundamental shift. Instead of passively waiting for crawlers to find and interpret your content, you can publish an explicit declaration of what your agent can do, and other agents can interact with it programmatically. This is the difference between having a brochure and having a conversation partner.

This guide walks through every aspect of building an A2A agent endpoint: the AgentCard specification, the JSON-RPC 2.0 task protocol, a complete Next.js implementation, authentication, CORS configuration, skills definition, testing procedures, and real-world examples. By the end, your website will be a participant in the agentic web, not just a bystander. Use our AgentReady scanner to check whether your site already has an A2A endpoint configured.

What is the A2A protocol?

The Agent-to-Agent protocol is an open standard that defines how AI agents discover each other and communicate. It was created by Google and contributed to the Linux Foundation for open governance in 2025. The protocol has two core components: discovery via an AgentCard, and communication via JSON-RPC 2.0 task messages.

Think of it this way. The current web is built for humans: HTML pages rendered in browsers, with search engines acting as intermediaries. The agentic web is being built for AI: structured capabilities declared in machine-readable formats, with agents acting as autonomous participants. A2A is the communication protocol that makes agent-to-agent interaction possible at scale.

Why A2A matters for your website

Without A2A, AI agents that want to interact with your business have to scrape your website, parse unstructured HTML, and guess at your capabilities. This is unreliable, slow, and limited. With A2A, an agent can:

  • Discover your capabilities instantly by reading your AgentCard at .well-known/agent.json
  • Understand what your agent can do through structured skill definitions with examples
  • Send specific tasks via a standardised JSON-RPC 2.0 interface
  • Receive structured responses that can be processed programmatically
  • Handle errors gracefully using standard error codes and messages

Early adopters of A2A are already seeing benefits. When an agent like Claude, ChatGPT, or a custom enterprise agent encounters a business with an A2A endpoint, it can provide richer, more accurate information to its users. This translates to better referrals, more qualified leads, and a competitive advantage over businesses that remain invisible to the agent ecosystem.

A2A vs MCP: understanding the difference

There are two major protocols shaping the agentic web, and they serve different purposes:

Aspect A2A (Agent-to-Agent) MCP (Model Context Protocol)
Purpose Agent-to-agent communication Model-to-tool connection
Relationship Peer-to-peer Client-server
Discovery .well-known/agent.json mcp.json manifest
Protocol JSON-RPC 2.0 JSON-RPC 2.0
Who controls execution The receiving agent decides how to process the task The calling model controls tool execution
Typical use case Agent asks another agent to complete a task AI model calls an external API or database

A2A and MCP are complementary. Your website should ideally implement both: MCP for tool-level access (see our MCP Server Guide) and A2A for agent-level communication. Together, they make your site fully accessible to the agentic web.

The AgentCard: declaring your agent's capabilities

The AgentCard is a JSON document served at /.well-known/agent.json that tells other agents who you are, what you can do, and how to interact with you. It follows the RFC 8615 well-known URI convention, the same pattern used by .well-known/openid-configuration and similar discovery endpoints.

AgentCard schema reference

Here is the complete AgentCard schema with every field explained:

{
  "name": "Your Agent Name",
  "description": "A clear, one-sentence description of what your agent does",
  "url": "https://yourdomain.com/api/agent",
  "provider": {
    "organization": "Your Company Name",
    "url": "https://yourdomain.com"
  },
  "version": "1.0.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false,
    "stateTransitionHistory": false
  },
  "authentication": {
    "schemes": ["bearer"]
  },
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "skills": [
    {
      "id": "skill-unique-id",
      "name": "Skill Display Name",
      "description": "What this skill does in detail",
      "tags": ["category", "domain"],
      "examples": [
        "Example prompt that would trigger this skill",
        "Another example prompt"
      ]
    }
  ]
}

Let us break down each field:

  • name -- The display name of your agent. Keep it short and recognisable. This is what other agents show when referencing yours.
  • description -- A concise summary of your agent's purpose. Other agents use this to decide whether to route tasks to you.
  • url -- The endpoint where task messages should be sent. This is your JSON-RPC 2.0 handler, typically /api/agent.
  • provider -- Your organisation details. Includes organization (company name) and url (company website).
  • version -- Semantic version of your agent. Increment when you change skills or capabilities.
  • capabilities -- Declares what communication patterns your agent supports. streaming for Server-Sent Events, pushNotifications for webhooks, stateTransitionHistory for full task state tracking.
  • authentication -- Declares which auth schemes your endpoint accepts. Common values: bearer, apiKey, or omit for public endpoints.
  • defaultInputModes -- What input formats your agent accepts: text, image, audio, video.
  • defaultOutputModes -- What output formats your agent produces.
  • skills -- An array of skill objects declaring specific capabilities. This is the most important section.

Defining effective skills

Skills are the heart of your AgentCard. Each skill represents a distinct capability your agent offers. Well-defined skills make it easy for other agents to route appropriate tasks to you.

Guidelines for skill definitions:

  1. Be specific, not generic. "Get current pricing for web development projects" is better than "Answer questions".
  2. Include 2-3 example prompts. These help other agents understand exactly when to invoke your skill.
  3. Use descriptive tags. Tags like pricing, scheduling, technical-support help with routing.
  4. Keep skill count manageable. 3-10 skills is the sweet spot. Too many dilutes clarity; too few limits usefulness.

Here is an example of a well-defined skills array for a digital agency:

"skills": [
  {
    "id": "get-services",
    "name": "Get Services",
    "description": "Returns a list of services offered by the agency, including AI development, web applications, mobile apps, and fractional CTO engagements",
    "tags": ["services", "capabilities", "offerings"],
    "examples": [
      "What services does p0stman offer?",
      "Do you build mobile apps?",
      "What kind of AI projects do you work on?"
    ]
  },
  {
    "id": "get-pricing",
    "name": "Get Pricing Information",
    "description": "Returns pricing tiers and engagement models including project-based, retainer, and fractional partnerships",
    "tags": ["pricing", "cost", "engagement"],
    "examples": [
      "How much does a website cost?",
      "What are your retainer options?",
      "Pricing for an AI agent build"
    ]
  },
  {
    "id": "get-case-studies",
    "name": "Get Case Studies",
    "description": "Returns details about completed projects including tech stack, outcomes, and client testimonials",
    "tags": ["portfolio", "work", "projects"],
    "examples": [
      "Show me your recent projects",
      "Do you have experience with healthcare AI?",
      "Case studies in fintech"
    ]
  },
  {
    "id": "schedule-consultation",
    "name": "Schedule Consultation",
    "description": "Initiates the process to book a discovery call with the team",
    "tags": ["booking", "consultation", "contact"],
    "examples": [
      "I want to book a call",
      "Schedule a consultation",
      "How do I get started with a project?"
    ]
  }
]

Placing the AgentCard file

For Next.js projects, place the file at public/.well-known/agent.json. Next.js serves files from the public directory as static assets, so this will be accessible at https://yourdomain.com/.well-known/agent.json.

For static sites or other frameworks, ensure the file is served at the exact path /.well-known/agent.json. Some hosting platforms may strip the .well-known directory by default -- verify this is not the case.

For middleware-based frameworks, you may need to add the path to your middleware bypass list. In Next.js, if your middleware intercepts requests, add .well-known/agent.json to the exclude list in your matcher configuration.

Building the JSON-RPC 2.0 task endpoint

The AgentCard tells other agents what your agent can do. The task endpoint is where the actual communication happens. A2A uses JSON-RPC 2.0 as its transport protocol, which provides a clean request/response pattern with standardised error handling.

JSON-RPC 2.0 request format

Every A2A task request follows this structure:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tasks/send",
  "params": {
    "id": "unique-task-id-from-sender",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "The actual question or task"
        }
      ]
    }
  }
}

Key fields:

  • jsonrpc -- Always "2.0". This is required by the JSON-RPC specification.
  • id -- A unique identifier for this request. The response must include the same ID so the sender can match responses to requests.
  • method -- The A2A method being called. tasks/send is the primary method for sending a task. Other methods include tasks/get (check task status) and tasks/cancel (cancel a running task).
  • params.id -- A unique task ID assigned by the sender. This persists across multiple exchanges for the same task.
  • params.message -- The message object containing the role (user or agent) and an array of parts. Parts can be text, image, or other supported media types.

JSON-RPC 2.0 response format

Your agent responds with a structured result:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "unique-task-id-from-sender",
    "status": {
      "state": "completed"
    },
    "artifacts": [
      {
        "parts": [
          {
            "type": "text",
            "text": "The agent's response to the task"
          }
        ]
      }
    ]
  }
}

Task states include:

  • completed -- The task was processed successfully. Artifacts contain the result.
  • failed -- The task could not be completed. Include an error message in the status.
  • working -- The task is being processed (for long-running tasks with streaming).
  • input-required -- The agent needs additional information before proceeding.

Error response format

When something goes wrong, return a JSON-RPC error:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "Invalid request: missing required field 'params.message'"
  }
}

Standard JSON-RPC 2.0 error codes:

Code Meaning When to use
-32700 Parse error Invalid JSON received
-32600 Invalid request Missing required fields
-32601 Method not found Unknown method name
-32602 Invalid params Wrong parameter types
-32603 Internal error Server-side failure

Complete Next.js implementation

Here is a production-ready A2A endpoint for Next.js App Router. This handles GET requests (returning the AgentCard), OPTIONS requests (CORS preflight), and POST requests (task execution).

Step 1: Create the AgentCard file

Create public/.well-known/agent.json:

{
  "name": "Your Company Agent",
  "description": "AI assistant for Your Company - answers questions about services, pricing, and projects",
  "url": "https://yourdomain.com/api/agent",
  "provider": {
    "organization": "Your Company",
    "url": "https://yourdomain.com"
  },
  "version": "1.0.0",
  "capabilities": {
    "streaming": false,
    "pushNotifications": false
  },
  "authentication": {
    "schemes": ["bearer"]
  },
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "skills": [
    {
      "id": "general-inquiry",
      "name": "General Inquiry",
      "description": "Answers questions about the company, services, and capabilities",
      "tags": ["info", "services"],
      "examples": [
        "What does your company do?",
        "Tell me about your services"
      ]
    }
  ]
}

Step 2: Create the API route

Create app/api/agent/route.ts:

import { NextRequest, NextResponse } from "next/server";

// CORS headers for browser-initiated A2A requests
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

// Your AgentCard - can also be loaded from the static file
const agentCard = {
  name: "Your Company Agent",
  description: "AI assistant for Your Company",
  url: "https://yourdomain.com/api/agent",
  provider: {
    organization: "Your Company",
    url: "https://yourdomain.com",
  },
  version: "1.0.0",
  capabilities: { streaming: false, pushNotifications: false },
  authentication: { schemes: ["bearer"] },
  defaultInputModes: ["text"],
  defaultOutputModes: ["text"],
  skills: [
    {
      id: "general-inquiry",
      name: "General Inquiry",
      description: "Answers questions about the company",
      tags: ["info"],
      examples: ["What does your company do?"],
    },
  ],
};

// GET - Return the AgentCard
export async function GET() {
  return NextResponse.json(agentCard, { headers: corsHeaders });
}

// OPTIONS - CORS preflight
export async function OPTIONS() {
  return new NextResponse(null, { status: 200, headers: corsHeaders });
}

// POST - Handle A2A task requests
export async function POST(req: NextRequest) {
  try {
    const body = await req.json();

    // Validate JSON-RPC 2.0 structure
    if (body.jsonrpc !== "2.0" || !body.method || !body.id) {
      return NextResponse.json(
        {
          jsonrpc: "2.0",
          id: body.id || null,
          error: { code: -32600, message: "Invalid JSON-RPC 2.0 request" },
        },
        { status: 400, headers: corsHeaders }
      );
    }

    // Route by method
    if (body.method === "tasks/send") {
      return handleTaskSend(body);
    }

    // Unknown method
    return NextResponse.json(
      {
        jsonrpc: "2.0",
        id: body.id,
        error: {
          code: -32601,
          message: `Method not found: ${body.method}`,
        },
      },
      { status: 404, headers: corsHeaders }
    );
  } catch (error) {
    return NextResponse.json(
      {
        jsonrpc: "2.0",
        id: null,
        error: { code: -32700, message: "Parse error" },
      },
      { status: 400, headers: corsHeaders }
    );
  }
}

async function handleTaskSend(body: any) {
  const taskId = body.params?.id;
  const message = body.params?.message;

  if (!message?.parts?.length) {
    return NextResponse.json(
      {
        jsonrpc: "2.0",
        id: body.id,
        error: {
          code: -32602,
          message: "Invalid params: message with parts is required",
        },
      },
      { status: 400, headers: corsHeaders }
    );
  }

  // Extract the text content from the message
  const textPart = message.parts.find(
    (p: any) => p.type === "text"
  );
  const taskText = textPart?.text || "";

  // Process the task - replace this with your AI logic
  const responseText = await processTask(taskText);

  return NextResponse.json(
    {
      jsonrpc: "2.0",
      id: body.id,
      result: {
        id: taskId,
        status: { state: "completed" },
        artifacts: [
          {
            parts: [{ type: "text", text: responseText }],
          },
        ],
      },
    },
    { headers: corsHeaders }
  );
}

async function processTask(taskText: string): Promise<string> {
  // Replace this with your actual AI processing logic
  // For example, call Gemini, OpenAI, or your own model
  return `Received your task: "${taskText}". This is a placeholder response.`;
}

Step 3: Add AI processing

The processTask function is where your agent's intelligence lives. In a production implementation, you would call an AI model (Gemini, GPT, Claude) with a system prompt that defines your agent's personality and knowledge base. Here is an example using the Google Gemini API:

import { GoogleGenerativeAI } from "@google/generative-ai";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

async function processTask(taskText: string): Promise<string> {
  const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });

  const systemPrompt = `You are the AI assistant for [Your Company].
You help visitors understand our services, pricing, and capabilities.
Be concise, helpful, and professional.

Company context:
- We build AI-powered applications and websites
- Services include: web development, AI agents, mobile apps
- Pricing starts from $3,000 for simple projects
- We offer monthly retainer partnerships at $2,500-3,000/month`;

  const result = await model.generateContent([
    { text: systemPrompt },
    { text: taskText },
  ]);

  return result.response.text();
}

Authentication for A2A endpoints

Authentication is declared in the AgentCard and enforced in your endpoint. There are three common approaches:

Public endpoints (no authentication)

For agents that answer general questions about your business, authentication may not be necessary. Omit the authentication field from your AgentCard, or set it to an empty object. This is the simplest approach and maximises accessibility.

Bearer token authentication

For agents that access sensitive data or perform actions, require a Bearer token in the Authorization header:

// In your POST handler, add auth check:
const authHeader = req.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
  return NextResponse.json(
    {
      jsonrpc: "2.0",
      id: body.id,
      error: { code: -32000, message: "Authentication required" },
    },
    { status: 401, headers: corsHeaders }
  );
}

const token = authHeader.slice(7);
const isValid = await validateToken(token);
if (!isValid) {
  return NextResponse.json(
    {
      jsonrpc: "2.0",
      id: body.id,
      error: { code: -32000, message: "Invalid token" },
    },
    { status: 403, headers: corsHeaders }
  );
}

API key authentication

A simpler alternative for server-to-server communication. Accept an API key in a custom header or query parameter:

const apiKey = req.headers.get("x-api-key");
if (apiKey !== process.env.A2A_API_KEY) {
  return NextResponse.json(
    {
      jsonrpc: "2.0",
      id: body.id,
      error: { code: -32000, message: "Invalid API key" },
    },
    { status: 403, headers: corsHeaders }
  );
}

CORS configuration for browser-initiated A2A

When another agent runs in a browser context (such as a Chrome extension or a web-based AI assistant), it needs CORS headers to access your endpoint. The implementation above includes CORS headers on every response, which is the recommended approach.

The three essential CORS headers for A2A:

  1. Access-Control-Allow-Origin: * -- Allows requests from any origin. For stricter security, replace * with specific allowed origins.
  2. Access-Control-Allow-Methods: GET, POST, OPTIONS -- A2A uses GET (AgentCard retrieval), POST (task execution), and OPTIONS (preflight).
  3. Access-Control-Allow-Headers: Content-Type, Authorization -- Allows JSON content type and Bearer token authentication.

The OPTIONS handler is critical. Without it, browsers will block POST requests from other origins because the preflight check fails. Many developers forget this and wonder why their A2A endpoint works with curl but not from browser-based agents.

Logging A2A interactions

Tracking which agents interact with yours is essential for understanding usage patterns and debugging issues. Create a database table to log all A2A sessions:

-- Supabase / PostgreSQL
CREATE TABLE agent_sessions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  task_id text,
  user_agent text,
  task_text text,
  response_text text,
  source text,
  created_at timestamptz DEFAULT now()
);

CREATE INDEX idx_agent_sessions_created_at
  ON agent_sessions (created_at DESC);

Add logging to your task handler:

// After processing the task, log the interaction
await supabase.from("agent_sessions").insert({
  task_id: taskId,
  user_agent: req.headers.get("user-agent") || "unknown",
  task_text: taskText,
  response_text: responseText,
  source: "a2a",
});

This gives you visibility into what agents are asking, how your agent responds, and which agent platforms are interacting with you most frequently.

Testing your A2A endpoint

Thorough testing ensures your agent works correctly before other agents try to communicate with it.

Test 1: Verify the AgentCard

# Fetch the AgentCard
curl -s https://yourdomain.com/.well-known/agent.json | jq .

# Verify required fields exist
curl -s https://yourdomain.com/.well-known/agent.json | jq '{
  name: .name,
  url: .url,
  skills_count: (.skills | length)
}'

Test 2: Send a basic task

curl -X POST https://yourdomain.com/api/agent \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tasks/send",
    "params": {
      "id": "test-001",
      "message": {
        "role": "user",
        "parts": [{"type": "text", "text": "What services do you offer?"}]
      }
    }
  }' | jq .

Test 3: Verify error handling

# Test invalid JSON-RPC
curl -X POST https://yourdomain.com/api/agent \
  -H "Content-Type: application/json" \
  -d '{"invalid": true}' | jq .

# Test unknown method
curl -X POST https://yourdomain.com/api/agent \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "nonexistent/method",
    "params": {}
  }' | jq .

Test 4: Verify CORS headers

# Check OPTIONS preflight
curl -X OPTIONS https://yourdomain.com/api/agent \
  -H "Origin: https://other-agent.com" \
  -H "Access-Control-Request-Method: POST" \
  -v 2>&1 | grep -i access-control

Test 5: Verify GET returns AgentCard

curl -s https://yourdomain.com/api/agent | jq .name

Real-world example: p0stman.com

The p0stman.com website has a live A2A endpoint that demonstrates these patterns in production. Here is what the actual AgentCard looks like:

# Fetch p0stman's AgentCard
curl -s https://p0stman.com/.well-known/agent.json | jq .

# Send a task to p0stman's agent
curl -X POST https://p0stman.com/api/agent \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tasks/send",
    "params": {
      "id": "demo-001",
      "message": {
        "role": "user",
        "parts": [{"type": "text", "text": "What services does p0stman offer?"}]
      }
    }
  }'

The p0stman agent processes the task using Gemini 2.0 Flash, with a system prompt loaded from a context document that contains full company information, pricing, case studies, and service descriptions. Every interaction is logged to the agent_sessions table for analytics.

The agent is also registered on a2aregistry.org, a public directory of A2A-compatible agents. Registering your agent increases discoverability and signals to the ecosystem that your site is agent-ready.

Advanced patterns

Multi-turn conversations

A2A supports multi-turn conversations by reusing the same task ID across multiple tasks/send calls. Your agent can return "state": "input-required" to signal that it needs more information:

// Return input-required state
return NextResponse.json({
  jsonrpc: "2.0",
  id: body.id,
  result: {
    id: taskId,
    status: {
      state: "input-required",
      message: {
        role: "agent",
        parts: [{
          type: "text",
          text: "I'd be happy to provide a quote. What type of project are you looking to build?"
        }]
      }
    }
  }
}, { headers: corsHeaders });

Streaming responses

For long-running tasks, A2A supports Server-Sent Events (SSE) streaming. Set "streaming": true in your AgentCard capabilities, and implement the tasks/sendSubscribe method. The calling agent receives incremental updates as your agent processes the task.

Push notifications

For tasks that take significant time (minutes or hours), A2A supports push notifications via webhooks. The calling agent provides a callback URL, and your agent POSTs status updates to that URL as the task progresses. Set "pushNotifications": true in capabilities to advertise this.

Skill-based routing

When your agent has multiple skills, route incoming tasks to the appropriate handler based on content analysis:

async function handleTaskSend(body: any) {
  const taskText = body.params?.message?.parts?.[0]?.text || "";

  // Simple keyword-based routing
  let handler: (text: string) => Promise<string>;

  if (/pricing|cost|price|how much/i.test(taskText)) {
    handler = handlePricingInquiry;
  } else if (/case study|portfolio|project|work/i.test(taskText)) {
    handler = handleCaseStudyInquiry;
  } else if (/book|schedule|call|consultation/i.test(taskText)) {
    handler = handleSchedulingRequest;
  } else {
    handler = handleGeneralInquiry;
  }

  const responseText = await handler(taskText);
  // ... return JSON-RPC response
}

For more sophisticated routing, use an AI model to classify the intent and route to the appropriate skill handler.

Deployment checklist

Before going live with your A2A endpoint, verify each of these items:

  1. AgentCard is accessible at /.well-known/agent.json and returns valid JSON
  2. All required fields are present: name, description, url, provider, version, skills
  3. Skills have examples -- at least 2 example prompts per skill
  4. GET /api/agent returns the AgentCard
  5. POST /api/agent accepts and processes JSON-RPC 2.0 tasks
  6. OPTIONS /api/agent returns proper CORS headers
  7. Error handling returns proper JSON-RPC error responses
  8. Logging captures task interactions for analytics
  9. Middleware does not block /.well-known/agent.json or /api/agent
  10. Authentication matches what is declared in the AgentCard

Use the AgentReady scanner to automate this verification. It checks for the AgentCard, validates the schema, tests the endpoint, and reports any issues.

Frequently Asked Questions

What is the A2A protocol?

The Agent-to-Agent (A2A) protocol is an open standard initiated by Google and now governed by the Linux Foundation that enables AI agents to discover each other's capabilities and communicate directly. It uses an AgentCard for discovery at .well-known/agent.json and JSON-RPC 2.0 for task execution. The protocol is designed to work across different AI platforms and frameworks, providing a universal language for agent communication.

Where should the AgentCard be placed?

The AgentCard must be served at /.well-known/agent.json on your domain. This follows the RFC 8615 well-known URI convention. For Next.js projects, place the file at public/.well-known/agent.json or serve it dynamically from an API route. Make sure your middleware or CDN does not strip or block requests to the .well-known directory.

What is JSON-RPC 2.0 and why does A2A use it?

JSON-RPC 2.0 is a lightweight remote procedure call protocol using JSON. A2A uses it because it provides a standardised request/response format with method names, parameters, and error handling. It is simpler than REST for this use case because every interaction follows the same structure. This makes it easy for agents built on different platforms to communicate without custom integration work for each pair of agents.

Do I need authentication for my A2A endpoint?

It depends on your use case. Public-facing agents that answer general questions about your business can work perfectly well without authentication, maximising the number of agents that can interact with you. For agents that access user data, modify resources, or perform sensitive operations, Bearer token or API key authentication is recommended. The AgentCard's authentication field declares which schemes your agent supports.

What is the difference between A2A and MCP?

A2A (Agent-to-Agent) enables communication between independent AI agents, where each agent has its own reasoning capabilities and decides how to process tasks. MCP (Model Context Protocol) connects an AI model to external tools and data sources that the model controls directly. A2A is peer-to-peer; MCP is client-server. They are complementary protocols -- implement both for maximum agentic web coverage. See our MCP Server Guide for details.

How do I test my A2A endpoint?

Test your AgentCard by fetching https://yourdomain.com/.well-known/agent.json with curl and verifying the JSON structure. Test your task endpoint by sending a JSON-RPC 2.0 request to your /api/agent route. Verify error handling with invalid requests. Check CORS with an OPTIONS request. The AgentReady scanner automates all of these checks as part of a full AI readiness audit.

Can I define multiple skills on one agent?

Yes. The AgentCard's skills array supports multiple skill definitions. Each skill has its own ID, name, description, tags, and example prompts. Your agent endpoint routes incoming tasks to the appropriate skill handler based on the task content. Most production agents define 3-10 skills covering their core capabilities. Keep skills focused and specific rather than broad and generic.

What CORS headers does an A2A endpoint need?

A2A endpoints should return Access-Control-Allow-Origin: * (or specific origins), Access-Control-Allow-Methods: GET, POST, OPTIONS, and Access-Control-Allow-Headers: Content-Type, Authorization. The OPTIONS preflight handler must return these headers with a 200 status for browser-initiated A2A requests to work. Without proper CORS, browser-based agents cannot communicate with your endpoint.

Related Guides

Paul Gosnell, founder of p0stman

Paul Gosnell · Founder, p0stman

Want to know how AI agents see your site?

AgentReady checks your site for the signals in this guide and tells you exactly what to fix. Free, no signup, results in under a minute.

Scan your site Free scan. No signup.