A2A Interoperability Reference: Bindings, Versions, Wire Differences
Two A2A implementations interoperate when they agree on four things: which interface to call, which protocol version to speak, how each operation looks in the chosen binding, and how errors are carried back. This page is the lookup for all four. It is built for the moment something does not connect, or when you must support v0.3 and v1.0 side by side.
Everything is checked against A2A v1.0.1 (a2a.proto and the specification at that tag), compared with v0.3.0 (specification/grpc/a2a.proto, specification/json/a2a.json, and that tag’s specification). The v1.0.1 service definition is identical to main at commit 98279d6. Where the specification and the proto disagree, the proto wins (section 1.4); the cases are listed in section 2
.
The objects inside the messages are covered elsewhere: Task Runtime Guide for states and events, Agent Card Proto Field Numbers for card fields, and A2A Error Codes for the nine errors.
1. Will the two sides connect?
Check in this order. The first failing step is the cause.
| Step | Check | Rule | If it fails |
|---|---|---|---|
| 1. Pick an interface | Read supportedInterfaces in order and take the first entry whose protocolBinding you support and whose protocolVersion you speak. Call that entry’s url | Sections 8.3.2, 3.6.2, and 3.6.3 | No usable entry: stop. The specification defines no error for this; section 5.2 only asks clients to implement fallback to another protocol |
| 2. Send the version | Send A2A-Version with the Major.Minor of that entry’s protocolVersion, such as 1.0, on every request | Sections 3.6.1 and 14.2.1 | VersionNotSupportedError. An empty value is read as 0.3 (section 3.6.2) |
| 3. Carry the tenant | Copy the entry’s tenant into the tenant field of every request message; omit the field when the entry has none | Section 8.3.2 | The specification defines no error for a wrong value; the server decides. See Multi-Tenancy |
| 4. Check capabilities | Read capabilities on the card before you stream, use push notifications, or fetch the extended card | Section 3.3.4 | UnsupportedOperationError, PushNotificationNotSupportedError, or ExtendedAgentCardNotConfiguredError |
| 5. Declare extensions | Send the extension URIs you support in A2A-Extensions, comma-separated | Sections 3.2.6 and 3.3.4 | ExtensionSupportRequiredError when the card marks an extension required: true and you did not declare it |
| 6. Read the error by binding | Decide by the A2A error, not by the HTTP or gRPC status alone | Sections 5.4 and 11.6 | See section 3 |
How a server reads A2A-Version
| Header sent | Interface exposes | Result | Rule |
|---|---|---|---|
| Absent or empty | 1.0 only | VersionNotSupportedError, because an empty value is read as 0.3 | Section 3.6.2 |
| Absent or empty | 0.3 | Processed with 0.3 semantics | Section 3.6.2 |
1.0 | 1.0 | Processed with 1.0 semantics | Section 3.6.2 |
0.3 | 1.0 only | VersionNotSupportedError | Section 3.6.2 |
0.3 | One 0.3 and one 1.0 interface, on the same or on different URLs | Each interface applies its own version; call the one that declares 0.3 | Section 3.6.2 |
2.0, or any value no interface declares | Any | VersionNotSupportedError | Section 3.6.2 |
1.0.1 | 1.0 | Not specified. Patch numbers must not take part in negotiation (section 3.6), but the registered header value is Major.Minor (section 14.2.1). Send 1.0 | Sections 3.6 and 14.2.1 |
2. The operations across the three bindings
JSON-RPC and gRPC use the same name. The HTTP+JSON routes and the return types come from the HTTP annotations and signatures of A2AService in a2a.proto at v1.0.1.
| Operation | JSON-RPC method and gRPC RPC | HTTP+JSON route | Returns |
|---|---|---|---|
| Send a message | SendMessage | POST /message:send | SendMessageResponse: a task or a message |
| Send and stream | SendStreamingMessage | POST /message:stream | Stream of StreamResponse |
| Get a task | GetTask | GET /tasks/{id} | Task |
| List tasks | ListTasks | GET /tasks | ListTasksResponse |
| Cancel a task | CancelTask | POST /tasks/{id}:cancel | Task |
| Subscribe to a task | SubscribeToTask | GET /tasks/{id}:subscribe | Stream of StreamResponse |
| Create a push config | CreateTaskPushNotificationConfig | POST /tasks/{task_id}/pushNotificationConfigs | TaskPushNotificationConfig |
| Get a push config | GetTaskPushNotificationConfig | GET /tasks/{task_id}/pushNotificationConfigs/{id} | TaskPushNotificationConfig |
| List push configs | ListTaskPushNotificationConfigs | GET /tasks/{task_id}/pushNotificationConfigs | ListTaskPushNotificationConfigsResponse |
| Delete a push config | DeleteTaskPushNotificationConfig | DELETE /tasks/{task_id}/pushNotificationConfigs/{id} | Empty |
| Get the extended card | GetExtendedAgentCard | GET /extendedAgentCard | AgentCard |
Notes:
- Every route also exists under a
/{tenant}prefix, for examplePOST /{tenant}/message:send. The proto declares it as an additional binding; the specification’s route list (section 11.3) shows only the tenant-less forms. The request message of each of the eleven operations has atenantfield. - The proto names the path variables
task_idandid; the specification’s table (section 5.3) writes{id}and{configId}. The URL shape is the same. GETandDELETEhave no body. Their parameters travel in the path or in camelCase query parameters, such aspageSizeandcontextId(section 11.5).
Where the documents disagree
| Topic | The documents | Follow |
|---|---|---|
SubscribeToTask over HTTP+JSON | Sections 5.3 and 11.3.2 say POST /tasks/{id}:subscribe; the proto annotation says GET | The proto. Handling is in the Task Runtime Guide |
JSON-RPC method | The base request structure (section 9.3) shows "method": "category/action", the v0.3 pattern. Sections 9.1 and 9.4 specify PascalCase names | Sections 9.1 and 9.4: SendMessage, GetTask, and so on |
A2A-Version in examples | The examples in sections 3.2.6, 9.2, 10.2, and 11.2 send 0.3; section 3.6.1 and the 1.0 card examples use 1.0 | Send the protocolVersion of the interface you call. Pasting the examples against a 1.0-only interface returns VersionNotSupportedError |
| Header registration | Section 14.2.1 names section 3.2.5 as the defining section; the service parameters are in section 3.2.6 | Section 3.2.6 |
| Tenant routes | Section 11.3 lists no /{tenant} routes; the proto has them | The proto |
3. What differs between the bindings
A server that supports several bindings must return equivalent results and map errors consistently (section 5.1). The transport details still differ.
| Aspect | JSON-RPC | gRPC | HTTP+JSON | Source |
|---|---|---|---|---|
| Call | POST to the interface url with {jsonrpc, id, method, params}; params is the request message | A unary or server-streaming RPC on A2AService | One route per operation (section 2 ) | Sections 9.4, 10.3, and 11.3 |
| Parameters | In params | In the request message | JSON body for POST; path and camelCase query for GET and DELETE | Sections 9.4 and 11.5 |
| Service parameters | HTTP headers, case-insensitive | gRPC metadata; keys arrive lowercase, such as a2a-version | HTTP headers, case-insensitive | Sections 9.2, 10.2, and 11.2 |
| Content type | application/json | Not specified | application/a2a+json (SHOULD) | Sections 9.1 and 11.1 |
| Tenant | params.tenant | tenant in the message | The tenant field, or the /{tenant}/... route prefix | Section 8.3.2 and the proto |
| Error carrier | error.code, error.message, and error.data, an array of typed objects | status.code, status.message, and status.details | HTTP status, plus error.code, error.message, and error.details | Sections 9.5, 10.6, and 11.6 |
| Which A2A error it is | The code, -32001 to -32009, is unique per error | Six errors share FAILED_PRECONDITION; read ErrorInfo.reason | Seven errors share 400; read ErrorInfo.reason | Sections 5.4, 10.6, and 11.6 |
| Streaming | SSE; each data: line is a complete JSON-RPC response whose result is a StreamResponse | A server-streaming RPC of StreamResponse | SSE; each data: line is a bare StreamResponse | Sections 9.4.2, 10.7, and 11.7 |
Two consequences follow. First, a client that parses JSON-RPC stream lines must unwrap result; a client that parses REST stream lines must not. Second, an HTTP 404 or 400 identifies an A2A error only when the body carries an ErrorInfo with domain set to a2a-protocol.org; a bare 404 from a proxy or a wrong route does not.
4. From v0.3 to v1.0.1 on the wire
Names and routes
For v0.3.0 the gRPC names and REST routes are read from the proto. The JSON-RPC names are the method constants in the JSON schema. For v1.0.1, JSON-RPC and gRPC use the operation name in section 2
.
| Operation | v0.3.0 JSON-RPC | v0.3.0 gRPC | v0.3.0 REST | v1.0.1 REST |
|---|---|---|---|---|
SendMessage | message/send | SendMessage | POST /v1/message:send | POST /message:send |
SendStreamingMessage | message/stream | SendStreamingMessage | POST /v1/message:stream | POST /message:stream |
GetTask | tasks/get | GetTask | GET /v1/tasks/{id} | GET /tasks/{id} |
ListTasks | none | none | none | GET /tasks |
CancelTask | tasks/cancel | CancelTask | POST /v1/tasks/{id}:cancel | POST /tasks/{id}:cancel |
SubscribeToTask | tasks/resubscribe | TaskSubscription | GET /v1/tasks/{id}:subscribe | GET /tasks/{id}:subscribe |
CreateTaskPushNotificationConfig | tasks/pushNotificationConfig/set | CreateTaskPushNotificationConfig | POST /v1/task/{id}/pushNotificationConfigs | POST /tasks/{task_id}/pushNotificationConfigs |
GetTaskPushNotificationConfig | tasks/pushNotificationConfig/get | GetTaskPushNotificationConfig | GET /v1/tasks/{id}/pushNotificationConfigs/{configId} | GET /tasks/{task_id}/pushNotificationConfigs/{id} |
ListTaskPushNotificationConfigs | tasks/pushNotificationConfig/list | ListTaskPushNotificationConfig | GET /v1/tasks/{id}/pushNotificationConfigs | GET /tasks/{task_id}/pushNotificationConfigs |
DeleteTaskPushNotificationConfig | tasks/pushNotificationConfig/delete | DeleteTaskPushNotificationConfig | DELETE /v1/tasks/{id}/pushNotificationConfigs/{configId} | DELETE /tasks/{task_id}/pushNotificationConfigs/{id} |
GetExtendedAgentCard | agent/getAuthenticatedExtendedCard | GetAgentCard | GET /v1/card | GET /extendedAgentCard |
What the v0.3.0 documents get wrong, and what to build against:
ListTasksdid not exist in v0.3.0. The specification’s method table (v0.3.0 section 3.5.6) and itstasks/listsection listListTaskandGET /v1/tasks, but the JSON schema defines no such method and the proto no such RPC. A v1.0 client that callsListTaskson a v0.3 agent has nothing to reach.- Push-config RPC names. The v0.3.0 table names four RPCs without the
Configsuffix (CreateTaskPushNotificationand its siblings). The proto, which is normative, has the suffix, and its list RPC is singular:ListTaskPushNotificationConfig. v1.0.1 makes it plural. - The create route. The v0.3.0 specification gives
POST /v1/tasks/{id}/pushNotificationConfigs. The v0.3.0 proto annotation is/v1/{parent=task/*/pushNotificationConfigs}, which expands totask, singular. SubscribeToTask. The v0.3.0 table saysPOST; the proto saysGET. The same mismatch survives in v1.0.1.- Resource names. v0.3.0 gRPC and REST requests identify a resource with one
namestring, such astasks/{id}ortasks/{id}/pushNotificationConfigs/{push_id}, or withparent. v1.0.1 uses separateidandtask_idfields.
Parameters that changed
These are the JSON-RPC params of v0.3.0 (from the JSON schema) against the request messages of v1.0.1 (from the proto, each with an added tenant).
| Operation | v0.3.0 params | v1.0.1 params | What breaks |
|---|---|---|---|
GetTask | id, historyLength, metadata | id, historyLength | metadata has no field to carry it |
SubscribeToTask | id, metadata | id | metadata has no field to carry it |
CancelTask | id, metadata | id, metadata | Nothing |
CreateTaskPushNotificationConfig | taskId, pushNotificationConfig holding id, url, token, authentication | taskId, id, url, token, authentication | The pushNotificationConfig wrapper is gone; a request in the old shape puts the URL where the server does not read it |
GetTaskPushNotificationConfig | id is the task ID; pushNotificationConfigId is the config ID; metadata | taskId is the task ID; id is the config ID | id changes meaning without changing name; metadata has no field |
DeleteTaskPushNotificationConfig | id is the task ID; pushNotificationConfigId is the config ID; metadata | taskId is the task ID; id is the config ID | Same silent change as the get call |
ListTaskPushNotificationConfigs | id is the task ID; metadata | taskId, pageSize, pageToken | id becomes taskId; the list is paged; metadata has no field |
The id rows are the dangerous ones. The old call is still well-formed JSON with a field named id, so nothing fails at the parser. The server reads that value as a config ID.
Other wire-level changes
| Area | v0.3.0 | v1.0.1 | Source |
|---|---|---|---|
| Version signal | The specification has no version header | A2A-Version on every request; an empty value is read as 0.3 | Section 3.6 |
| Interface declaration | url, preferredTransport, and additionalInterfaces; one protocolVersion for the card | supportedInterfaces, in preference order; protocolVersion per entry as Major.Minor | v0.3.0 section 5.6; sections 8.3.1 and 8.3.2 |
| REST content type | application/json (MUST) | application/a2a+json (SHOULD) | v0.3.0 section 3.2.3; section 11.1 |
| REST route prefix | /v1 | None; a version belongs to the interface | v0.3.0 section 3.5.3; section 5.3 |
| Tenant | No tenant field | tenant in every request message, and in AgentInterface | Section 8.3.2 |
| JSON-RPC error codes | -32001 to -32007; -32007 is AuthenticatedExtendedCardNotConfiguredError | The same numbers; -32007 is ExtendedAgentCardNotConfiguredError; -32008 and -32009 are new | v0.3.0 section 8; section 5.4 |
| Error details | The data field can carry details; its structure is not defined | data is an array of typed objects, with ErrorInfo.reason and domain for A2A errors | Sections 9.5 and 11.6 |
| REST streaming | SSE “similar to JSON-RPC”; the data format is not defined | Each data: line is a bare StreamResponse | v0.3.0 section 3.3.3; section 11.7 |
| Proto location | specification/grpc/a2a.proto | specification/a2a.proto | v0.3.0 section 3.2.2; section 10.1 |
The message-level changes (kind, enum spelling, parts, final, and the blocking default) are in the Task Runtime Guide
. The card-level changes are in Agent Card Proto Field Numbers
.
5. When interop fails
| What you see | Likely cause | Fix |
|---|---|---|
JSON-RPC -32601 (MethodNotFoundError) for SendMessage | The agent is v0.3 and serves message/send | Read the card. Call the interface that declares 1.0, or use the v0.3 method names for a v0.3 interface |
JSON-RPC -32601 for message/send | The agent is v1.0 and serves SendMessage | Same, in the other direction |
gRPC UNIMPLEMENTED for ListTasks, SubscribeToTask, or GetExtendedAgentCard | A gRPC server answers a method it does not define with UNIMPLEMENTED (a gRPC rule, not an A2A one). A v0.3 server has none of these names | Check the interface’s protocolVersion; use TaskSubscription and GetAgentCard against v0.3 |
REST 404 on /v1/message:send or on /message:send | The route prefix differs between the versions | v0.3: /v1/...; v1.0: no prefix. A 404 is an A2A TaskNotFoundError only when error.details holds an ErrorInfo with reason TASK_NOT_FOUND |
VersionNotSupportedError | The header is missing, names a version the interface does not declare, or was copied from the specification’s 0.3 example | Send the entry’s protocolVersion as Major.Minor (section 1
) |
A gRPC server treats every call as 0.3 although the client sets the version | gRPC lowercases metadata keys, so the key arrives as a2a-version. A server that looks up A2A-Version finds nothing and reads an empty value | Look up the lowercase key (section 10.2) |
An HTTP 400 that does not match the error you expected | Seven errors share 400 | Read error.details[].reason (section 11.6) |
| A push-config get or delete finds nothing, or the wrong config | A v0.3 client sends the task ID in id; v1.0 reads id as the config ID | Send taskId and id as in section 4 |
| A stream parser fails on the first line | JSON-RPC lines are wrapped in {jsonrpc, id, result}; REST lines are a bare StreamResponse | Unwrap result for JSON-RPC only |
SubscribeToTask over HTTP+JSON is refused with a method error | The specification says POST; the proto says GET | Call GET; if the peer answers with a method error, try the other verb |
| The client lands on an old version although the agent also serves 1.0 | One card lists the same binding twice, with protocolVersion 0.3 and 1.0, and the client took the first entry | Choose by protocolVersion, not by position alone (section 3.6.2) |
The client cannot find supportedInterfaces | The card is a v0.3 card with url and preferredTransport | Read it by the v0.3 rules (v0.3.0 section 5.6.3), or ask the owner to upgrade. See the Agent Card Implementation Guide |
6. Interop test scenarios
Run these against a server (S) or a client (C). Server scenarios can be driven with curl or any gRPC client. Client scenarios need a card to read and, for I14, a peer that answers with a method error.
| ID | Side | Scenario | Expected | Source |
|---|---|---|---|---|
| I1 | S | Send the same message over every binding the card declares | Semantically equivalent results | Section 5.1 |
| I2 | S | Provoke the same failure, such as an unknown task ID, over every binding | The binding’s native code (-32001, NOT_FOUND, 404) plus an ErrorInfo with reason TASK_NOT_FOUND and domain a2a-protocol.org | Sections 5.4, 9.5, 10.6, and 11.6 |
| I3 | S | Call a 1.0-only interface with no A2A-Version | VersionNotSupportedError | Section 3.6.2 |
| I4 | S | Call it with A2A-Version: 1.0, then with 9.9 | The first succeeds; the second returns VersionNotSupportedError | Section 3.6.2 |
| I5 | S | Call it with A2A-Version: 1.0.1 | Record the result; the specification does not define it | Sections 3.6 and 14.2.1 |
| I6 | S | Send the version as a2a-version in HTTP headers, and as a2a-version in gRPC metadata | Handled exactly like A2A-Version | Sections 3.2.6, 9.2, 10.2, and 11.2 |
| I7 | S | Send A2A-Extensions with two URIs, comma-separated; omit a required extension | Both URIs are seen; the omission returns ExtensionSupportRequiredError | Sections 3.3.4 and 9.2 |
| I8 | S | With a tenant on the interface, call over HTTP+JSON once with the tenant field and once with the /{tenant} route prefix | Both reach the same tenant | Section 8.3.2 and the proto |
| I9 | C | Read a card that lists JSONRPC first and GRPC second, with a client that speaks only gRPC | The client calls the second entry’s url | Section 8.3.2 |
| I10 | C | Read a card with two entries for one binding, 0.3 and 1.0 | The client calls the entry that matches the version it sends | Sections 3.6.2 and 3.6.3 |
| I11 | S | Stream over JSON-RPC and over HTTP+JSON | JSON-RPC lines parse as JSON-RPC responses; REST lines are bare StreamResponse objects | Sections 9.4.2 and 11.7 |
| I12 | S | GET /tasks?contextId=...&status=TASK_STATE_WORKING&pageSize=10 | The filters and the page size apply | Section 11.5 |
| I13 | S | Add an unknown field to a request | The request is handled; the field is ignored | Section 5.7 |
| I14 | C | Subscribe over HTTP+JSON | The client calls GET; on a method error it retries with POST | Sections 5.3 and 11.3.2, and the proto |
Source: A2A official specification: Protocol Binding Requirements and Interoperability (5), Versioning (3.6), Agent Card Protocol Declaration (8.3), and the JSON-RPC, gRPC, and HTTP+JSON bindings (9 to 11)
; a2a.proto at v1.0.1
and v0.3.0
; the v0.3.0 JSON schema
and specification