Dissecting A2A's protocol layer: the five core JSON-RPC methods from message/send, tasks/get, tasks/cancel, tasks/pushNotificationConfig, to messages/list, complete with HTTP POST binding practice and the SSE endpoint for streaming.

In episode 4 we understood Task and Message as the data model. Now it's time to see how that model is called: the JSON-RPC methods available and how those methods attach to HTTP.
Episode 5 is the most "protocol-heavy" episode so far. You'll memorize the five core methods, then practice them directly with curl against an A2A server. After this episode, you'll be able to trace A2A traffic like reading a book.
A2A uses JSON-RPC 2.0 as its invocation language. Every request is an object with three required fields:
{
"jsonrpc": "2.0",
"id": "call-001",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{ "text": "Halo" }]
}
}
}jsonrpc — always the value 2.0.id — the request's unique marker; the response returns the same id so the client can match them up.method — the name of the method being called.params — the method's arguments; their shape is specific to each method.Unlike REST, which has many endpoints per resource, JSON-RPC uses just one endpoint with different methods inside the body. This simplicity is what makes cross-transport binding (HTTP, gRPC, SSE) possible.
The most important method — used to send the first message and create a new task at the same time. If params.taskId is absent, the remote agent creates a new task and returns its id.
curl -s -X POST https://analisis.example.com \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"role":"user","parts":[{"text":"Analisis laporan ini"}]}}}'The response carries the task object complete with its id and initial status:
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"id": "task-7f3a",
"status": { "state": "submitted" }
}
}From this response the client gets the taskId that will be used in all subsequent methods.
To fetch a task's current state — including its message history — the client calls tasks/get with a taskId. This method is essential for the polling pattern when streaming is unavailable.
{
"jsonrpc": "2.0",
"id": "2",
"method": "tasks/get",
"params": {
"taskId": "task-7f3a",
"historyLength": 10
}
}The historyLength parameter limits the number of messages returned. For tasks with long conversations, this prevents the payload from bloating.
When a task is no longer relevant — for example, requirements change midway — the client cancels it with tasks/cancel:
{
"jsonrpc": "2.0",
"id": "3",
"method": "tasks/cancel",
"params": {
"taskId": "task-7f3a"
}
}Cancellation changes the task status to canceled. A good remote agent will stop in-progress work and free its resources.
This method switches the communication mode from client pulling (polling) to agent pushing (push). The client registers a webhook URL the remote agent will call whenever the task updates:
{
"jsonrpc": "2.0",
"id": "4",
"method": "tasks/pushNotificationConfig",
"params": {
"taskId": "task-7f3a",
"pushNotificationConfig": {
"url": "https://client.example.com/hooks/a2a"
}
}
}Once registered, the remote agent sends a notification to that URL every time the task status changes — without waiting for the client to ask. The full details of streaming and push, including retry and polling fallback, will be explored in episode 7.
A task's complete conversation history can be fetched with messages/list. Unlike tasks/get, which carries a status snapshot, this method focuses on all the messages:
{
"jsonrpc": "2.0",
"id": "5",
"method": "messages/list",
"params": {
"taskId": "task-7f3a"
}
}The response is an ordered array of messages — from the user's first message to the agent's last. This method is useful for rebuilding a conversation's context from scratch, for example when a client joins a task that has been running for a while.
All the methods above are carried over POST to a single endpoint — usually the URL listed in the Agent Card. Summary:
POST https://analisis.example.com -> all JSON-RPC methods
GET https://analisis.example.com/.well-known/agent-card, /sseThe rules are consistent with the HTTP specification: a Content-Type: application/json header, a JSON-RPC request body, and responses return with status 200 as long as the request is valid — even when the task fails, because the failure is carried in the JSON-RPC body, not the HTTP status code.
Warning
Don't get into the habit of relying on HTTP status codes to judge a task's outcome. A 200 code only means "request received and processed", not "task succeeded". For that, read result.status.state in the response body.
If the Agent Card declares streaming: true, the client can open an SSE channel to receive task updates in real time:
curl -s -N https://analisis.example.com/sse?taskId=task-7f3aevent: task_update
data: {"state":"working","message":{"role":"agent","parts":[{"text":"Menganalisis..."}]}}
event: task_update
data: {"state":"completed","message":{"role":"agent","parts":[{"text":"Selesai!"}]}}Each event carries a message delta or a status change. Try it directly in your terminal with curl -s -N https://analisis.example.com/sse?taskId=task-7f3a. The client closes the channel after receiving a terminal status (completed, failed, or canceled). The combination of POST for requests and SSE for notifications is what's called the HTTP+SSE binding.
Here's the core takeaway:
jsonrpc, id, method, and params fields.message/send creates a task and sends the first message.tasks/get and tasks/cancel manage tasks: read the state and cancel.tasks/pushNotificationConfig registers a webhook so the agent pushes updates.messages/list fetches the complete conversation history.In episode 6 we move from theory to real practice: Python & TypeScript SDK — installing a2a-sdk for Python and @a2a-js/sdk for TypeScript, writing task handlers with decorators, and running your first A2A agent. See you there!