5.2

HTTP+JSON

HTTP+JSON names each operation with a verb and a path from the proto's google.api.http annotations, and sends the request and response messages as plain JSON bodies.

The planner repeats its first test run over the second interface of test-runner, http://localhost:41241/a2a/rest with the binding HTTP+JSON. The same agent and task store answer, in 16-rest-binding.http, and only the envelope differs from 03-blocking-task.http.

When you finish this section, you can map any operation to its route, set its content type, and send a tenant when the card asks.

Routes from the proto

Every rpc of A2AService carries an option (google.api.http) with a verb and a path template, and §11.3 lists the same routes. An operation with a request body is a POST with body: "*": /message:send, /message:stream, /tasks/{id}:cancel, and /tasks/{task_id}/pushNotificationConfigs. Reads are GET, and the configuration delete is DELETE. Operations by binding gives all eleven routes with their messages.

For subscribe the proto says get: "/tasks/{id=*}:subscribe", and §5.3 and §11.3.2 say POST /tasks/{id}:subscribe (conflict D1). The reference SDK's server accepts both verbs, and its client sends POST. The Java and .NET servers accept only POST. So accept both on a server, and send POST from a client.

SendMessage over HTTP+JSON, with no envelopehttpFull log
POST /a2a/rest/message:send HTTP/1.1
A2A-Version: 1.0
Content-Type: application/a2a+json

{
  "message": {
    "messageId": "48f165d5-7b00-47f4-b81e-f86f5c8cc1ab",
    "role": "ROLE_USER",
…
HTTP/1.1 200 OK
Content-Type: application/a2a+json

{
  "task": {
    "id": "bbfb9ea5-50ea-4441-b991-2a8ba0d6f3f3",
    "contextId": "41c0d462-fed8-4f6b-8569-6c9afe9497c3",
…
Host and the rest of both bodies are cut. Over JSON-RPC the same reply sits under result, in 03-blocking-task.http.

Query parameters and content types

GET and DELETE carry no body, so their request fields travel as path parameters or query parameters, in camelCase. GetTask puts the task id in the path and historyLength in the query. ListTasks takes contextId, status, pageSize, pageToken, historyLength, statusTimestampAfter, and includeArtifacts. A nested object cannot go in a query string, so that request must be a POST. Figure 5.1 sets both operations beside their JSON-RPC form.

Fig. 5.1SendMessage and GetTask in two JSON bindingscomparison
One operation in two JSON bindings Two rows, SendMessage and GetTask, each headed by its gRPC rpc line from a2a.proto. In each row the left column shows the JSON-RPC request, a POST to /a2a/jsonrpc whose body names the method and carries the request message in params, and a reply whose result holds the response message. The right column shows HTTP+JSON, where the verb and path name the operation, the body or the path and query carry the request, and the reply body is the response message itself. The steps appear in capture order: each request, then its reply. A · JSON-RPC B · HTTP+JSON gRPC rpc SendMessage(SendMessageRequest) returns (SendMessageResponse) gRPC rpc GetTask(GetTaskRequest) returns (Task) POST /a2a/jsonrpc Content-Type: application/json "jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": SendMessageRequest HTTP/1.1 200 OK "jsonrpc": "2.0", "id": 1, "result": SendMessageResponse POST /a2a/rest/message:send Content-Type: application/a2a+json body: SendMessageRequest HTTP/1.1 200 OK Content-Type: application/a2a+json body: SendMessageResponse POST /a2a/jsonrpc "jsonrpc": "2.0", "id": 3, "method": "GetTask", "params": {"id": "d0c28826", "historyLength": 0} HTTP/1.1 200 OK "jsonrpc": "2.0", "id": 3, "result": Task GET /a2a/rest/tasks/bbfb9ea5 ?historyLength=0 no body: the path and query carry the request HTTP/1.1 200 OK Content-Type: application/a2a+json body: Task
JSON-RPC posts every operation to one URL and names it in the body, HTTP+JSON names it with a verb and a path, and gRPC calls an rpc. Each row is one operation, headed by its rpc line from a2a.proto. Bold marks where each binding names the operation, and italic type names stand for the message that fills that place. Left, the blocking-task and polling captures. Right, the HTTP+JSON capture. Ids shortened to 8 characters.

Requests and responses should use application/a2a+json, and the kit sends it on every body. The reference SDK sends its errors as application/json, and the Java and Go servers send every body that way. So send application/a2a+json, and accept both types when you read.

Streams and errors

A stream is the same text/event-stream response as in JSON-RPC. Each data: line holds the StreamResponse alone, with no envelope. No capture records a stream in this binding, so streaming and subscribing shows the JSON-RPC frames.

An error uses the HTTP status code and the JSON form of google.rpc.Status in an error object. Exchanges 3 and 4 of the record show two, and Errors reads that body.

The tenant field

Tenant: an optional opaque string on an interface that routes requests to one agent, or to one tenant when several agents share one endpoint. The same tenant field sits on all ten request messages, from SendMessageRequest to GetExtendedAgentCardRequest. A client copies the card's value into every request, and must leave the field out when the entry sets no tenant. The protocol gives the value no format, and no kit card sets one.

Here the value can also travel in the path. Every rpc has a second google.api.http form with a /{tenant} prefix, such as post: "/{tenant}/message:send". Only the proto lists these routes, and §5.3, §11.3, and §11.5 show the plain ones. In JSON-RPC the value is params.tenant, and in gRPC it is the message field. The migration guide describes this as tenant scoping built into gRPC requests, which understates it.

The project's guide names three routing keys for several agents on one host: a URL prefix, the credential, and the tenant field. A deployment can use more than one of them.

The reference SDK's client wraps the transport in a TenantTransportDecorator when the chosen interface sets a tenant. Its HTTP+JSON transport puts the value in the path prefix. Its server mounts every route again under /{tenant}, copies the value into the call context, and never compares it with the card. The agent reads RequestContext.tenant and decides what it means.

A2A protocol 1.0.1 · 3303592 · 2026-05-28 · Edition 2026.10