What Is A2A and How Do You Implement It in 2026?
A2A lets one agent hand a job to another agent it does not own, then follow that job to completion over a standard task lifecycle. By the end of this page you will be able to delegate a task with curl, publish your own agent as an A2A server, and collect long-running results on a webhook.
A2A (Agent2Agent Protocol) is an open protocol that lets one AI agent delegate a task to another independent agent over HTTP, without either side exposing its model, its tools or its internal state. It defines a discovery document, a Task object with an explicit set of lifecycle states, and three interchangeable transport bindings: JSON-RPC 2.0, gRPC and HTTP+JSON/REST. It is built for teams whose agents need to work with agents that somebody else wrote and operates.
Why A2A matters for AI visibility
Most GEO work points inward: making a page legible to a model that is reading the web. A2A points outward. It makes a service you operate callable by an agent that somebody else operates. An assistant working on a customer's behalf usually does not want your HTML at all, it wants to hand your agent a job and come back for the result. A2A gives that handover a fixed shape: a discovery document at a well-known path, a Task with states the caller can reason about, and typed content parts for text, files and structured data. The discovery document is covered in depth on the Agent Card page, so this page stays on the protocol and the delegation flow.
It is also no longer one vendor's proposal. Google contributed the protocol to the Linux Foundation, and the Foundation's April 2026 update put the project past 150 supporting organisations with the protocol embedded in the major cloud platforms. Version 1.0.0 is the first stable specification, which is the point at which the wire format becomes worth building against rather than tracking. If you already run an MCP server, the two are not competitors: MCP connects a model to the tools and data you control, A2A connects your agent to agents you do not control. A2A servers routinely call MCP internally to get the work done.
Where the spec lives
The normative text, the reference SDKs and the debugging tools all sit in one GitHub organisation, and the rendered specification is versioned so you can pin the exact release you built against.
- A2A Protocol Specification v1.0.0 - the normative document: data model, operations, task states, all three protocol bindings, and the security sections. Pin the versioned copy at /v1.0.0/ when you need a stable reference.
- What's New in v1.0 - the full 0.3 to 1.0 rename list. Read this before you port anything written against an older tutorial, because most of the breakage is silent.
- a2aproject/A2A - the specification source, release notes, and the Protocol Buffer definitions the JSON model is generated from. Useful when a field name is ambiguous in prose.
- a2a-js SDK - the official TypeScript server and client, published as @a2a-js/sdk, with runnable samples for streaming, cancellation, push notifications and card signing.
- A2A Inspector - a browser tool that fetches an agent card, runs spec compliance checks against it, and shows the raw JSON-RPC traffic while you chat to the agent.
- Linux Foundation project announcement - the governance position, for when procurement asks who actually owns the standard you are adopting.
Three ways to implement A2A
Start at the wire, because every SDK is a thin layer over it. The first example delegates a task to somebody else's agent using nothing but curl and jq, which is also the fastest way to test a partner integration. The second publishes your own agent as an A2A server on Node with the official TypeScript SDK, which is what you want if other people's agents need to reach you. The third handles the case that breaks naive clients: work that takes minutes or hours, delivered to a webhook instead of an open socket.
Delegate a task to another agent with curl
Use this when you are integrating against an agent someone else runs and want to see the real request and response before you reach for a client library. It uses the HTTP+JSON/REST binding, which is the easiest of the three to inspect.
#!/usr/bin/env bash
set -euo pipefail
AGENT="https://agent.example.com"
# 1. Discovery. Read the card, then take the HTTP+JSON base URL from it.
CARD=$(curl -fsS "$AGENT/.well-known/agent-card.json")
BASE=$(echo "$CARD" | jq -r '[.supportedInterfaces[]
| select(.protocolBinding == "HTTP+JSON") | .url][0]')
# 2. Delegate. returnImmediately stops the call blocking until the task ends.
TASK=$(curl -fsS -X POST "$BASE/message:send" \
-H 'Content-Type: application/a2a+json' \
-H 'A2A-Version: 1.0' \
-H "Authorization: Bearer $AGENT_TOKEN" \
-d '{
"message": {
"messageId": "'"$(uuidgen)"'",
"role": "ROLE_USER",
"parts": [{ "text": "Summarise our Q3 returns policy changes." }]
},
"configuration": {
"acceptedOutputModes": ["text/plain"],
"returnImmediately": true
}
}' | jq -r '.task.id')
# 3. Poll GetTask until the task stops moving. Results arrive as artifacts.
while true; do
STATE_JSON=$(curl -fsS "$BASE/tasks/$TASK" \
-H 'A2A-Version: 1.0' -H "Authorization: Bearer $AGENT_TOKEN")
case "$(echo "$STATE_JSON" | jq -r '.status.state')" in
TASK_STATE_COMPLETED)
echo "$STATE_JSON" | jq -r '.artifacts[].parts[].text'; break ;;
TASK_STATE_FAILED|TASK_STATE_CANCELED|TASK_STATE_REJECTED)
echo "ended without a result"; exit 1 ;;
TASK_STATE_INPUT_REQUIRED|TASK_STATE_AUTH_REQUIRED)
echo "agent is waiting on you"; exit 1 ;;
*) sleep 2 ;;
esac
done
What this does: it performs the whole delegation loop an autonomous agent would perform, in the same order: find the interface on the card, submit a message that creates a Task, then watch the task state until it reaches a terminal or interrupted state. Note that INPUT_REQUIRED and AUTH_REQUIRED are not failures, they are the agent asking for a second turn.
Publish your own agent as an A2A server on Node
Use this when other people's agents need to reach a capability you run. The official TypeScript SDK gives you one request handler and mounts it on as many transports as you want to advertise, so you write the business logic once.
// npm install @a2a-js/sdk express
import express from 'express';
import { A2A_PROTOCOL_VERSION, AGENT_CARD_PATH, AgentCard } from '@a2a-js/sdk';
import { DefaultRequestHandler, InMemoryTaskStore } from '@a2a-js/sdk/server';
import { agentCardHandler, jsonRpcHandler, restHandler, UserBuilder }
from '@a2a-js/sdk/server/express';
import { ReturnsPolicyExecutor } from './executor.js';
const card: AgentCard = {
name: 'Returns Policy Agent',
description: 'Answers questions about our returns, refunds and exchanges.',
supportedInterfaces: [
{ url: 'https://agent.example.com/a2a/jsonrpc', protocolBinding: 'JSONRPC',
tenant: '', protocolVersion: A2A_PROTOCOL_VERSION },
{ url: 'https://agent.example.com/a2a/rest', protocolBinding: 'HTTP+JSON',
tenant: '', protocolVersion: A2A_PROTOCOL_VERSION },
],
provider: { organization: 'Example Retail', url: 'https://www.example.com' },
version: '1.4.0',
capabilities: { streaming: true, pushNotifications: true,
extensions: [], extendedAgentCard: false },
securitySchemes: {}, securityRequirements: [], signatures: [],
defaultInputModes: ['text/plain'], defaultOutputModes: ['text/plain'],
skills: [{
id: 'returns_policy',
name: 'Returns policy lookup',
description: 'Explains return windows, refund timings and exclusions.',
tags: ['returns', 'refunds', 'policy'],
examples: ['How long do I have to return a jacket bought on sale?'],
inputModes: ['text/plain'], outputModes: ['text/plain'],
securityRequirements: [],
}],
};
const handler = new DefaultRequestHandler(
card, new InMemoryTaskStore(), new ReturnsPolicyExecutor());
const app = express();
app.use(`/${AGENT_CARD_PATH}`, agentCardHandler({ agentCardProvider: handler }));
app.use('/a2a/jsonrpc', jsonRpcHandler(
{ requestHandler: handler, userBuilder: UserBuilder.noAuthentication }));
app.use('/a2a/rest', restHandler(
{ requestHandler: handler, userBuilder: UserBuilder.noAuthentication }));
app.listen(41241);
What this does: it serves the card at /.well-known/agent-card.json and exposes the same agent over two bindings from one DefaultRequestHandler, which is what the spec's functional equivalence rule requires. Your ReturnsPolicyExecutor implements AgentExecutor and publishes Task, status and artifact events onto the event bus the handler hands it.
Collect long-running results on a push notification webhook
Use this when the work takes longer than a request should stay open, or when the delegating process is a batch job that cannot hold a stream. The client registers a webhook with the message, the agent POSTs every event to it, and nobody holds a socket.
import express from 'express';
const AGENT = 'https://agent.example.com/a2a/rest';
const WEBHOOK = 'https://ops.example.com/a2a/task-updates';
const TOKEN = process.env.A2A_WEBHOOK_TOKEN;
const HEADERS = { 'Content-Type': 'application/a2a+json', 'A2A-Version': '1.0' };
const ours = new Set();
// 1. Delegate, naming the webhook. The task id comes back straight away.
export async function delegate(text) {
const res = await fetch(`${AGENT}/message:send`, {
method: 'POST', headers: HEADERS,
body: JSON.stringify({
message: { messageId: crypto.randomUUID(), role: 'ROLE_USER', parts: [{ text }] },
configuration: {
returnImmediately: true,
taskPushNotificationConfig: { url: WEBHOOK, token: TOKEN },
},
}),
});
const { task } = await res.json();
ours.add(task.id);
}
// 2. Receive. The body carries exactly one of task, message,
// statusUpdate or artifactUpdate at the top level.
const app = express();
app.use(express.json({ type: ['application/json', 'application/a2a+json'] }));
app.post('/a2a/task-updates', async (req, res) => {
if (req.get('X-A2A-Notification-Token') !== TOKEN) return res.sendStatus(401);
const event = req.body.statusUpdate ?? req.body.artifactUpdate ?? req.body.task;
const taskId = event?.taskId ?? event?.id;
if (!ours.has(taskId)) return res.sendStatus(202);
res.sendStatus(200); // ack first: delivery is at least once, so expect repeats
if (req.body.statusUpdate?.status?.state === 'TASK_STATE_COMPLETED') {
const r = await fetch(`${AGENT}/tasks/${taskId}`, { headers: HEADERS });
const task = await r.json();
console.log(task.artifacts.flatMap((a) => a.parts)
.map((p) => p.text).filter(Boolean).join('\n'));
ours.delete(taskId);
}
});
app.listen(8080);
What this does: it turns a blocking call into an event-driven one. The webhook payload is a notification, not the result, so the handler fetches the finished Task to read its artifacts. The token header name is the reference SDK's default rather than a fixed part of the spec, so confirm it with whichever agent you are calling, and treat the token as a rotating secret.
Implementation guidelines
These are the things that break in production rather than in the tutorial.
- Send the version header on every request. A2A v1.0 clients must set
A2A-Version: 1.0, using the Major.Minor form only. An empty header is interpreted as version 0.3, so forgetting it silently downgrades you to the old semantics instead of erroring. - Never block a socket on work you cannot bound. Send Message blocks by default until the task reaches a terminal or interrupted state. Set
returnImmediately: trueand then poll GetTask, subscribe to the stream, or register a push notification config. - Declare capabilities honestly, because they gate operations. Streaming requires
capabilities.streamingand webhooks requirecapabilities.pushNotificationson the card. Calling a streaming method against an agent that has not declared it returnsUnsupportedOperationError, JSON-RPC code-32004. - Cache the card, and give clients validators. Serve an
ETagderived from the card version plus aCache-Controlmax-age that matches how often your skills actually change, and honourIf-None-Match. Clients should use conditional requests rather than refetching the whole card per call. - Return results as artifacts, not as messages. The spec is explicit that messages carry conversation and progress while artifacts carry output, and that not every message is guaranteed to persist in task history. A client that scrapes results out of status messages will lose data on reconnect.
- Harden webhooks on both ends. As the agent, validate the client's webhook URL against private and link-local ranges before calling it, or you have built an SSRF proxy. As the receiver, check the token, match the task id against a task you started, answer 2xx, and process idempotently.
- Read the endpoint from the card, never from memory. Clients must parse
supportedInterfaces, take the first entry whose binding they support, use that entry's exact URL, and copy itstenantvalue into every request. Hard-coded paths break the moment the agent moves or adds a transport.
Do this, not that
Do
- Serve the card at
/.well-known/agent-card.jsonover HTTPS with anETagand a sensibleCache-Controlmax-age. - List every interface in
supportedInterfacesin genuine preference order, each with its own exact URL andprotocolVersion. - Handle all eight task states, including
TASK_STATE_INPUT_REQUIREDandTASK_STATE_AUTH_REQUIRED, which are turns rather than failures. - Catch
VersionNotSupportedError(-32009) and fall back to an older version deliberately, logging it, rather than negotiating down by accident.
Do not
- Do not send lowercase enum values such as
"completed"or"user". Version 1.0 serialises enums asTASK_STATE_COMPLETEDandROLE_USER. - Do not put
mimeTypeor akinddiscriminator on a part. Version 1.0 usesmediaTypeand a baretext,url,rawordatamember. - Do not advertise a second transport you have not tested. All declared bindings must offer identical operations, behaviour, errors and auth schemes.
- Do not trust a webhook POST because it reached your URL. Verify the token header and match the task id before you act on the payload.
How Glippy checks this
Glippy scores A2A readiness under Agent Interactivity, category 10. It requests /.well-known/agent-card.json on the domain, checks that it returns JSON rather than an SPA shell or a soft 404, and looks for the fields a calling agent needs before it can do anything: a supportedInterfaces array with a reachable URL and a declared binding, a populated skills list, and capabilities that match the operations the endpoint actually answers. Broken or missing cards surface alongside the other machine-facing signals in the AI agent accessibility checker.
Check your A2A setup
Glippy runs 240+ checks across 16 categories on any page, including Agent Interactivity (category 10). No sign-up required.
Frequently asked questions
No, they solve different problems and are commonly deployed together. MCP standardises how a model reaches tools, APIs and data sources that you control, while A2A standardises how two independent agents discover each other and delegate work as peers. A typical A2A server receives a task over A2A and then calls several MCP tools internally to fulfil it. The A2A specification itself describes the two as complementary in its appendix on the relationship to MCP.
The well-known URI is https://{server_domain}/.well-known/agent-card.json, and the specification registers that path with IANA. Registries and direct configuration are also valid discovery routes, but the well-known path is what an agent will try first with no prior knowledge of you. The filename was agent.json in early versions and was renamed in 0.3, so guides that still point at the old name predate that change. The Agent Card page covers the document itself in detail.
No. One binding is enough, as long as you declare it accurately in supportedInterfaces on your agent card. Clients are required to read that list, pick the first entry whose binding they support, and use that entry's URL. If you do advertise more than one, the specification requires them to be functionally equivalent: same operations, same behaviour, same error mappings and the same authentication schemes.
Quite a lot, and most of it fails quietly. Method names moved from strings like message/send and tasks/get to PascalCase SendMessage and GetTask; enum values became SCREAMING_SNAKE_CASE, so completed is now TASK_STATE_COMPLETED; preferredTransport and additionalInterfaces collapsed into a single supportedInterfaces array that also carries protocolVersion; mimeType became mediaType; and the kind discriminator was dropped from parts and stream events. A ListTasks operation was added. The official What's New in v1.0 page carries the complete list.
Reviewed against the primary sources on . These standards move quickly, so check the linked specs before you ship.