A2A protocol bindings: JSON-RPC, gRPC, and HTTP+JSON
What an A2A protocol binding is, how JSON-RPC, gRPC and HTTP+JSON map methods and errors, how Agent Cards declare them, and how version negotiation works.
A protocol binding is the concrete wire format for A2A. Version 1.0 of the specification defines every operation once, in abstract terms, with a2a.proto as the normative data model. Bindings then map those operations onto a transport: method names or URLs, how headers travel, how errors are encoded, and how streaming works. The specification defines three standard bindings: JSON-RPC 2.0 over HTTP, gRPC over HTTP/2, and HTTP+JSON, a REST-style mapping. Anyone can define more.
An agent declares its bindings in the supportedInterfaces list of its Agent Card, one entry per endpoint, binding and protocol version. Every binding an agent offers must provide the same operations with the same behavior, the same error meanings and the same authentication (section 5.1). So the choice between them comes down to your infrastructure.
What a binding covers
| JSON-RPC | gRPC | HTTP+JSON | |
|---|---|---|---|
| Transport | JSON-RPC 2.0 over HTTP(S) | gRPC over HTTP/2 with TLS | HTTP(S) |
| Operation naming | PascalCase methods, such as SendMessage |
RPCs of the A2AService service |
HTTP verb plus resource path, such as POST /message:send |
| Payload | application/json |
Protocol Buffers 3 | application/a2a+json (SHOULD) |
| Service parameters | HTTP headers | gRPC metadata, lowercased keys | HTTP headers |
| Errors | JSON-RPC error object with a data array |
google.rpc.Status with details |
HTTP status plus a google.rpc.Status JSON body |
| Streaming | Server-Sent Events, each event a JSON-RPC response | Server-streaming RPC | Server-Sent Events, each event a StreamResponse |
| Spec section | 9 | 10 | 11 |
Service parameters are the protocol’s horizontal settings. The two standard ones are A2A-Version and A2A-Extensions (section 3.2.6). Their names are case-insensitive, and a list such as several extension URIs goes in one comma-separated value.
The HTTP+JSON media type changed during the 1.0 line. The v1.0.1 patch release made application/a2a+json the preferred content type for that binding. JSON-RPC stays on application/json.
Method mapping
Section 5.3 maps each operation across the three bindings.
| Operation | JSON-RPC method | gRPC method | HTTP+JSON endpoint |
|---|---|---|---|
| Send message | SendMessage |
SendMessage |
POST /message:send |
| Send streaming message | SendStreamingMessage |
SendStreamingMessage |
POST /message:stream |
| Get task | GetTask |
GetTask |
GET /tasks/{id} |
| List tasks | ListTasks |
ListTasks |
GET /tasks |
| Cancel task | CancelTask |
CancelTask |
POST /tasks/{id}:cancel |
| Subscribe to task | SubscribeToTask |
SubscribeToTask |
POST /tasks/{id}:subscribe (see inconsistencies) |
| Create push config | CreateTaskPushNotificationConfig |
same | POST /tasks/{id}/pushNotificationConfigs |
| Get push config | GetTaskPushNotificationConfig |
same | GET /tasks/{id}/pushNotificationConfigs/{configId} |
| List push configs | ListTaskPushNotificationConfigs |
same | GET /tasks/{id}/pushNotificationConfigs |
| Delete push config | DeleteTaskPushNotificationConfig |
same | DELETE /tasks/{id}/pushNotificationConfigs/{configId} |
| Get extended Agent Card | GetExtendedAgentCard |
same | GET /extendedAgentCard |
JSON-RPC and gRPC share method names, which keeps the two close. HTTP+JSON has three extra rules:
- GET and DELETE requests carry parameters in the path or the query string, with camelCase names that match the JSON fields, such as
GET /tasks/{id}?historyLength=10(section 11.5). Nested objects can’t go in a query string, so operations that need them use POST. - The proto adds a tenant form of every path, such as
POST /{tenant}/message:send, for agents that share one endpoint. - Version 1.0 dropped the
/v1prefix that 0.3 put in front of these paths. A version can still be part of the base URL if the agent owner wants one.
Error mapping
Every A2A error has a code, a message and optional details, and each binding maps them to its native form. Section 5.4 gives the table.
| A2A error | JSON-RPC | gRPC status | HTTP status |
|---|---|---|---|
TaskNotFoundError |
-32001 |
NOT_FOUND |
404 Not Found |
TaskNotCancelableError |
-32002 |
FAILED_PRECONDITION |
400 Bad Request |
PushNotificationNotSupportedError |
-32003 |
FAILED_PRECONDITION |
400 Bad Request |
UnsupportedOperationError |
-32004 |
FAILED_PRECONDITION |
400 Bad Request |
ContentTypeNotSupportedError |
-32005 |
INVALID_ARGUMENT |
400 Bad Request |
InvalidAgentResponseError |
-32006 |
INTERNAL |
500 Internal Server Error |
ExtendedAgentCardNotConfiguredError |
-32007 |
FAILED_PRECONDITION |
400 Bad Request |
ExtensionSupportRequiredError |
-32008 |
FAILED_PRECONDITION |
400 Bad Request |
VersionNotSupportedError |
-32009 |
FAILED_PRECONDITION |
400 Bad Request |
JSON-RPC also keeps its standard codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params and -32603 internal error. The A2A codes sit in the range JSON-RPC 2.0 reserves for implementation-defined server errors. Authentication and authorization failures have no A2A code. Section 3.3.2 suggests 401 or UNAUTHENTICATED and 403 or PERMISSION_DENIED, and leaves JSON-RPC to a custom error.
Because several errors share FAILED_PRECONDITION or 400, the status alone is ambiguous. gRPC and HTTP+JSON responses must include a google.rpc.ErrorInfo detail whose reason names the error in upper snake case without the “Error” suffix, such as TASK_NOT_FOUND, with the domain a2a-protocol.org. JSON-RPC carries detail objects in error.data. The specification says implementations should use ErrorInfo there, and its A2A error example does.
Here is one error in all three forms. The JSON-RPC response is real, from Emissar’s agent on 2026-09-26, after a SendMessage with the header A2A-Version: 0.5. The agent returned it with HTTP status 200.
{
"jsonrpc": "2.0",
"id": 9,
"error": {
"code": -32009,
"message": "Protocol version 0.5 is not supported. Supported: 1.0, 0.3",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org"
}
]
}
}
Illustrative: the same error over HTTP+JSON, sent with status 400 Bad Request.
{
"error": {
"code": 400,
"status": "FAILED_PRECONDITION",
"message": "Protocol version 0.5 is not supported. Supported: 1.0, 0.3",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org"
}
]
}
}
Illustrative: the same error over gRPC, in Protocol Buffers text form.
status {
code: FAILED_PRECONDITION
message: "Protocol version 0.5 is not supported. Supported: 1.0, 0.3"
details: [
{
type: "type.googleapis.com/google.rpc.ErrorInfo"
reason: "VERSION_NOT_SUPPORTED"
domain: "a2a-protocol.org"
}
]
}
Declaring bindings in the Agent Card
Each supportedInterfaces entry has four fields.
| Field | Required | Notes |
|---|---|---|
url |
Yes | Where this interface listens |
protocolBinding |
Yes | JSONRPC, GRPC, HTTP+JSON, or a URI for a custom binding |
protocolVersion |
Yes | Major.Minor, such as "1.0" |
tenant |
No | Opaque routing value; if set, clients send it in every request |
The list is ordered by preference. A client must pick the first entry it supports, use that entry’s URL, and send the entry’s tenant if it has one (section 8.3.2). The same URL may appear more than once if several bindings share an endpoint. An agent can also list the same binding twice with different protocol versions.
Illustrative: an agent that offers all three bindings on 1.0.
{
"supportedInterfaces": [
{ "url": "https://agent.example.com/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" },
{ "url": "agent.example.com:443", "protocolBinding": "GRPC", "protocolVersion": "1.0" },
{ "url": "https://agent.example.com/a2a/rest", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0" }
]
}
A custom binding should be named by a URI, and a breaking change to it needs a new URI (section 5.8). It must implement every core operation, keep the data model, and document how it carries service parameters, errors, streaming and authentication (section 12). If it can’t stream, it must say so. The A2A project hosts custom bindings under a cpb- repository prefix, with experimental ones as experimental-cpb-.
Version negotiation with A2A-Version
A protocol version is Major.Minor. Patch numbers don’t affect compatibility and must not be considered when clients and servers negotiate (section 3.6).
- Clients must send
A2A-Versionwith every request. The exception is 0.3 clients, because 0.3 had no such header. A client may send the version as a request parameter instead of a header. - Servers must process a request with the semantics of the version it names. If the interface doesn’t support that version, the server must return
VersionNotSupportedError. A missing or empty value must be read as 0.3. - SDKs must help clients manage versions. A client that needs newer features should request that version explicitly and not fall back to an older one automatically, which would lose features silently.
JSON-RPC and HTTP+JSON send the version as an HTTP header, A2A-Version: 1.0. gRPC sends it as the metadata key a2a-version. An agent that serves 0.3 and 1.0 side by side can list one interface per version, at the same URL or different ones. The 0.3 to 1.0 migration guide shows how one endpoint answers both.
Known inconsistencies in the specification
The text on main disagrees with itself in a few places. As of 2026-09-26:
| Topic | What the text says | What else says otherwise |
|---|---|---|
| gRPC interface URL | The sample Agent Card in section 8.5 uses https://georoute-agent.example.com/a2a/grpc |
The AgentInterface.url comment in a2a.proto asks for hostname:port, such as grpc.example.com:443 |
| HTTP+JSON subscribe verb | Sections 5.3 and 11.3.2 say POST /tasks/{id}:subscribe |
The HTTP annotation in a2a.proto says get |
| HTTP+JSON error body | Section 6.4’s version example returns application/problem+json in the RFC 9457 shape |
Section 11.6 and What’s New in v1.0 specify the google.rpc.Status JSON shape |
Section 1.4 makes a2a.proto the single authoritative definition of the data objects and messages. For the gRPC URL, that points to host:port. The subscribe verb is less clear, since the verb comes from the binding sections as much as the proto. A server can accept both verbs on that path until the text is fixed; clients generated from the proto will send GET.
How to choose
| Binding | Fits well when | Watch for |
|---|---|---|
| JSON-RPC | You want one URL, plain HTTP tooling and the shortest path to a working agent | All calls go to one URL as POST bodies, so HTTP caches and verb- or path-based gateway rules see little |
| gRPC | Your services already use gRPC and Protocol Buffers, and you want generated, typed clients | Needs HTTP/2 with TLS (section 10.1); metadata keys arrive lowercased |
| HTTP+JSON | Your API gateway, WAF or logging works on HTTP verbs, paths and status codes | Query-parameter mapping rules, and the subscribe verb conflict above |
Start with one binding and add others when a real caller needs them. The functional equivalence rule means a second binding is a transport adapter over the same logic, and clients are told to fall back between declared bindings (section 5.2). Emissar’s own public agent declares a single interface: JSON-RPC, protocol version 1.0, at https://emissar.ai/a2a/v1.
Whichever binding you pick, push notifications don’t use it. Webhook calls always use plain HTTP with the JSON payloads of the HTTP+JSON binding (section 3.5.1).
Questions
- Does an A2A agent have to support all three bindings?
- No. An agent must declare every binding it supports in supportedInterfaces, and each one must offer the same operations and behavior. Clients may pick any declared binding and should be able to fall back to another.
- Do JSON field names change between bindings?
- No. JSON-RPC and HTTP+JSON both use camelCase field names and ProtoJSON enum strings such as TASK_STATE_COMPLETED. gRPC sends the same proto messages in binary Protocol Buffers.
- Which binding carries push notifications?
- None of them. Webhook calls always use plain HTTP with the JSON payloads of the HTTP+JSON binding, whichever binding the client used to create the task.
Sources
- A2A Protocol Specification (sections 3.2.6, 3.3, 3.6, 5, 8.3, 9, 10, 11 and 12) (accessed )
- A2A protocol definition (a2a.proto): A2AService HTTP annotations, AgentInterface (accessed )
- What's new in A2A v1.0 (A2A documentation) (accessed )
- A2A v1.0.1 release notes (prefer application/a2a+json in the HTTP binding) (accessed )
- A2A Extension and Protocol Binding Governance (accessed )
- JSON-RPC 2.0 Specification (accessed )
- Emissar Agent Card (live example agent) (accessed )