The Agent-to-Agent protocol (A2A) answers one narrow question: how does an agent built by one team or vendor hand work to an agent built by another, without either side seeing the other's prompts, tools or memory? It does that with a small set of things: a discovery document called the Agent Card, a message format made of typed parts, a task object with an explicit state machine, and three ways to follow a task: wait, stream or receive webhooks. Everything else people attribute to A2A, such as budgets, cost attribution and allow-lists, belongs to the platform you run around it.

This page describes the protocol as published in specification version 1.0.0, which clients announce with the header A2A-Version: 1.0. Version 1.0 renamed the JSON-RPC methods and the task state values compared with 0.3, so code and older articles written against 0.3 (including some on this site) use names such as message/send and working. The migration section at the end lists the differences. By the end you should be able to read an Agent Card, trace a task from submission to artifact, and decide where policy enforcement has to live.

What A2A 1.0 standardises (blue) and what your platform adds (amber)Client agentorchestrator, plannerRemote agentA2A server, opaque insideAgent Card/.well-known/agent-card.json1 discoverpublishesAuth (securitySchemes)OAuth2, OIDC, mTLS, API key2 credentialsSendMessage / SendStreamingMessageMessage{role, parts[]} -> Task or Message3 requestTask state machine + ArtifactsSUBMITTED, WORKING, INPUT_REQUIRED ... COMPLETEDSSE streamStreamResponse eventsPush webhookStreamResponse via POSTYour platform, outside the specegress gateway, allow-list of remote agents, budgets, rate limits, audit log, trace propagation, cost attributionclient sideserver side
A client agent discovers a remote agent through its Agent Card, authenticates, sends a message and follows the resulting task by waiting, streaming or webhook. Budgets, allow-lists and audit live in your platform, not in the protocol.

What A2A standardises, and what it leaves to you

A2A treats a remote agent as an opaque service: the client never learns which model, framework or tools it uses, so the remote side can change its implementation without breaking callers. The specification defines a data model (Agent Card, Message, Part, Task, Artifact and the streaming events), a set of abstract operations, and three bindings for them: JSON-RPC 2.0, gRPC and HTTP+JSON. It requires TLS in production and says how an agent declares its authentication requirements, but leaves identity, authorization decisions and billing to existing web security practice.

What A2A standardises, and what it leaves to you

A2A treats a remote agent as an opaque service: the client never learns which model, framework or tools it uses, so the remote side can change its implementation without breaking callers. The specification defines a data model (Agent Card, Message, Part, Task, Artifact and the streaming events), a set of abstract operations, and three bindings for them: JSON-RPC 2.0, gRPC and HTTP+JSON. It requires TLS in production and says how an agent declares its authentication requirements, but leaves identity, authorization decisions and billing to existing web security practice.

Discovery: reading an Agent Card

Discovery starts with the Agent Card, a JSON document an agent serves at https://{domain}/.well-known/agent-card.json (registries and direct configuration are also allowed). Pre-0.3 implementations used agent.json; if a client fetches that path and gets a 404, that is usually the reason. A trimmed card in the 1.0 field layout looks like this:

{
  "name": "Hotel Booking Agent",
  "description": "Searches and books hotel rooms for a traveller.",
  "version": "2.3.0",
  "supportedInterfaces": [
    {"url": "https://hotels.example.com/a2a/v1", "protocolBinding": "JSONRPC",   "protocolVersion": "1.0"},
    {"url": "https://hotels.example.com/a2a/json", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0"}
  ],
  "capabilities": {"streaming": true, "pushNotifications": true, "extendedAgentCard": true},
  "securitySchemes": {
    "oidc": {"openIdConnectSecurityScheme": {
      "openIdConnectUrl": "https://login.example.com/.well-known/openid-configuration"}}
  },
  "defaultInputModes": ["text/plain", "application/json"],
  "defaultOutputModes": ["application/json"],
  "skills": [{
    "id": "search-and-book",
    "name": "Search and book a hotel",
    "description": "Finds rooms matching dates, city and budget, then books on confirmation.",
    "tags": ["travel", "hotel"],
    "examples": ["Two nights in Munich from 12 March under 200 EUR per night"]
  }]
}

Read a card in this order. supportedInterfaces tells you which bindings exist and at which URL; pick the first one your client supports. capabilities tells you whether streaming and push are allowed, which decides how you will follow long tasks. securitySchemes says how to obtain credentials, which happens out of band. skills are descriptive, not callable functions: a skill is a hint for a planner or a human, and the request is still a message. When extendedAgentCard is true, an authenticated client can call GetExtendedAgentCard to see skills that are not public. Cards should be served with Cache-Control and an ETag; cache them, and refetch when a call fails with a version or capability error. The Agent Card article covers the fields in more detail; note that it predates 1.0 in places.

The data model: messages, parts, tasks, artifacts

Four objects carry all the work. A Message is one turn: it has a required messageId, a role of ROLE_USER or ROLE_AGENT, and a list of Parts. In 1.0 a Part holds exactly one of text, raw (bytes), url (a file by reference) or data (arbitrary JSON), plus optional mediaType and filename. A Task is the unit of stateful work: an id, a contextId that groups related tasks into one conversation, a status with the current state and an optional status message, plus artifacts and history. An Artifact is an output, made of parts, with its own artifactId so it can be streamed in chunks.

A remote agent may answer a message with a Message (a quick, stateless reply) or a Task (anything that takes time, needs input or produces artifacts). Clients must handle both. Follow-up turns reuse contextId and, when continuing a specific task, set taskId on the new message; referenceTaskIds lets a message point at earlier tasks without continuing them.

The task state machine

The task state machine is the part of A2A that saves you the most design time, because every remote agent reports progress in the same vocabulary. Tasks start in TASK_STATE_SUBMITTED, move to TASK_STATE_WORKING, and end in one of four terminal states: TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED or TASK_STATE_REJECTED. Two states are interrupted rather than terminal: TASK_STATE_INPUT_REQUIRED (the agent needs more information from the client) and TASK_STATE_AUTH_REQUIRED (it needs additional credentials, for example a user consent).

Three rules follow from the specification and are worth encoding in your client. A terminal task accepts no further messages; sending one returns UnsupportedOperationError, so start a new task in the same context instead. A blocking SendMessage returns when the task reaches a terminal or interrupted state, so "the call returned" does not mean "the work is done". And cancellation is a request: CancelTask on a finished task returns TaskNotCancelableError (code -32002). The task lifecycle article walks through each transition.

One operation set, three bindings

Every operation exists in all three bindings with the same semantics. Choose JSON-RPC for the widest SDK support, gRPC for internal high-volume traffic, and HTTP+JSON when you want ordinary REST tooling and caches in front of the agent.

OperationJSON-RPC methodHTTP+JSON
Send a messageSendMessagePOST /message:send
Send and streamSendStreamingMessagePOST /message:stream
Read a taskGetTaskGET /tasks/{id}
List tasksListTasksGET /tasks
CancelCancelTaskPOST /tasks/{id}:cancel
Re-attach to a streamSubscribeToTaskPOST /tasks/{id}:subscribe
Register a webhookCreateTaskPushNotificationConfigPOST /tasks/{id}/pushNotificationConfigs
Private cardGetExtendedAgentCardGET /extendedAgentCard

A2A errors use JSON-RPC codes from -32001 upward: -32001 task not found, -32003 push not supported, -32004 unsupported operation, -32005 content type not supported, -32009 version not supported. The REST binding maps the same errors to HTTP status codes such as 404 and 400.

Following a task: wait, poll, stream or push

There are three ways to follow a task, and the right choice depends on how long it runs and whether the client can keep a connection open.

  • Blocking call. Default SendMessage waits for a terminal or interrupted state. Fine for work measured in seconds; put a client timeout on it that is shorter than any proxy idle timeout in the path.
  • Return immediately and poll. Set returnImmediately: true in the message configuration, then call GetTask with backoff. Simple and robust, at the cost of latency and wasted requests.
  • Stream. SendStreamingMessage returns Server-Sent Events. Each event is a StreamResponse holding exactly one of task, message, statusUpdate or artifactUpdate; the stream closes when the task reaches a terminal state. If the connection drops, SubscribeToTask re-attaches to a task that is still running.
  • Push. For tasks that run for minutes or hours, register a webhook. The agent POSTs the same StreamResponse objects to your URL, carrying the token you supplied so you can reject forged calls. Requires capabilities.pushNotifications.

More on the last two in A2A streaming and A2A push notifications.

Worked example: delegating a hotel booking

A travel planner needs a hotel. It has fetched the card above and holds an OIDC access token for the user. It sends a streaming request over JSON-RPC:

POST /a2a/v1 HTTP/1.1
Host: hotels.example.com
Authorization: Bearer eyJ...
A2A-Version: 1.0
Content-Type: application/json

{"jsonrpc": "2.0", "id": 7, "method": "SendStreamingMessage",
 "params": {"message": {
   "messageId": "m-01", "role": "ROLE_USER", "contextId": "trip-4411",
   "parts": [
     {"text": "Two nights in Munich from 12 March, under 200 EUR per night."},
     {"data": {"city": "Munich", "checkIn": "2027-03-12", "nights": 2, "maxPrice": 200, "currency": "EUR"}}
   ]}}}

The stream begins with the task, then status updates. Midway the agent needs a choice from the user, so it moves to an interrupted state and the stream ends with that event:

data: {"jsonrpc":"2.0","id":7,"result":{"task":{"id":"t-93","contextId":"trip-4411","status":{"state":"TASK_STATE_SUBMITTED"}}}}
data: {"jsonrpc":"2.0","id":7,"result":{"statusUpdate":{"taskId":"t-93","contextId":"trip-4411","status":{"state":"TASK_STATE_WORKING"}}}}
data: {"jsonrpc":"2.0","id":7,"result":{"statusUpdate":{"taskId":"t-93","contextId":"trip-4411",
       "status":{"state":"TASK_STATE_INPUT_REQUIRED","message":{"messageId":"m-02","role":"ROLE_AGENT",
       "parts":[{"text":"Two options: Hotel A 180 EUR, Hotel B 195 EUR with breakfast. Which one?"}]}}}}}

The planner asks the user, then continues the same task by sending a new message with taskId: "t-93". The agent books, emits an artifactUpdate with the confirmation as a data part, and finishes in TASK_STATE_COMPLETED. A minimal client loop looks like this:

import httpx, json, uuid

def send_streaming(base_url, token, text, context_id, task_id=None):
    msg = {"messageId": str(uuid.uuid4()), "role": "ROLE_USER",
           "contextId": context_id, "parts": [{"text": text}]}
    if task_id:
        msg["taskId"] = task_id
    body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()),
            "method": "SendStreamingMessage", "params": {"message": msg}}
    headers = {"Authorization": f"Bearer {token}", "A2A-Version": "1.0"}
    with httpx.stream("POST", base_url, json=body, headers=headers, timeout=60) as r:
        for line in r.iter_lines():
            if not line.startswith("data:"):
                continue
            event = json.loads(line[5:])
            if "error" in event:
                raise RuntimeError(event["error"])
            yield event["result"]          # one of task / message / statusUpdate / artifactUpdate

TERMINAL = {"TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"}

Trust: what is signed and what is not

A2A signs exactly one thing: the Agent Card. A card may carry signatures, each a JSON Web Signature (RFC 7515) computed over the card after JSON Canonicalization Scheme (RFC 8785) canonicalization. Verifying it tells you the card came from the key holder and was not altered by a mirror or registry. Messages and tasks are not signed by the protocol; their integrity comes from TLS and their authenticity from the credentials on each request. If you need non-repudiation of individual requests, for disputes or regulated workflows, add it yourself, for example by signing payloads and storing them in an append-only audit log.

Authorization is entirely the server's job. The specification requires the server to authenticate every request and calls authorization logic implementation-specific, based on skills, actions and scopes. In practice that means scoped OAuth tokens, a check that the caller owns the task ID it is asking about, and tenant isolation in storage. Webhooks are the commonly missed surface: validate the push URL against an allow-list to avoid server-side request forgery, and authenticate to it. Securing A2A endpoints with OAuth 2.0 shows a full setup.

The platform around the protocol

In production the protocol sits inside a platform layer, drawn in amber in the diagram. On the calling side an egress gateway holds the allow-list of remote agents a given internal agent may call, attaches credentials, enforces a per-workflow budget of calls and spend, and records every request with the tenant, workflow and trace ID. Budgets and deadlines are not A2A fields; you can pass them in metadata if both sides agree, but the client must enforce them itself by cancelling or abandoning tasks.

On the serving side, persist tasks in a store keyed by tenant and task ID, because streams drop and GetTask must answer after a restart. Propagate W3C traceparent headers so one trace spans both organisations' systems, and limit concurrent tasks per caller.

Migrating from 0.3 to 1.0

Moving a 0.3 client or server to 1.0 is mostly renaming, but the renames break silently if one side upgrades first. The clean path is to serve both: list a 0.3 and a 1.0 entry in supportedInterfaces, route on the A2A-Version header (empty means 0.3), and remove the old interface when traffic stops.

Concern0.31.0
Methodsmessage/send, message/stream, tasks/get, tasks/cancel, tasks/resubscribeSendMessage, SendStreamingMessage, GetTask, CancelTask, SubscribeToTask
Task state valuesworking, input-required ...TASK_STATE_WORKING, TASK_STATE_INPUT_REQUIRED ...
Rolesuser, agentROLE_USER, ROLE_AGENT
Version negotiationimplicitA2A-Version header, VersionNotSupportedError

Failure modes

  • Treating return as completion. A blocking call returned TASK_STATE_INPUT_REQUIRED and the client read the empty artifacts as the result. Branch on state, always.
  • Lost streams. A proxy closed an idle SSE connection after 60 seconds and the client gave up. Re-attach with SubscribeToTask, or fall back to GetTask.
  • Duplicate work on retry. A timeout made the client resend the same request as a new message, and the agent booked twice. Reuse the messageId on retries and make the server deduplicate on it.
  • Unbounded delegation. Agent A asks B, which asks C, which asks A. Carry a hop count or a delegation chain in metadata and refuse beyond a limit.

Trade-offs: A2A, MCP and plain APIs

A2A is not a replacement for tool protocols. The Model Context Protocol connects one agent to tools and data it controls; A2A connects agents that each keep their own reasoning, so it adds tasks, interrupted states and long-running delivery that tool calls do not need. Many systems use both: MCP inside each agent and A2A between them, as described in A2A and MCP interop patterns. A plain REST API is still simpler for a deterministic service with a fixed schema.

What to do next

  1. Fetch the Agent Card of every remote agent you call and record its binding, capabilities and auth scheme.
  2. Send A2A-Version: 1.0 on every request and handle VersionNotSupportedError.
  3. Write the client as a state machine over the eight task states, with separate paths for terminal and interrupted.
  4. Pick one follow strategy per skill: blocking, poll, stream with re-subscribe, or push.
  5. Verify card signatures when present and allow-list push URLs.
  6. Put budgets, allow-lists, audit and tracing in a gateway, and document that they are not protocol features.
  7. Persist tasks server-side and deduplicate on messageId.
Key takeaway: A2A 1.0 standardises discovery through an optionally signed Agent Card, messages made of typed parts, a task with eight states, and three ways to follow it, over JSON-RPC, gRPC or REST. It does not sign messages, set budgets or decide who may call whom; those belong to the gateway and policy layer you build around it. Branch on task state rather than on call return, re-attach to dropped streams, deduplicate on message IDs, and negotiate the version explicitly.