Streaming and push notifications in A2A
How A2A streams task updates over SSE, which events arrive in what order, how to reconnect, how push webhooks work and are secured, and when to poll instead.
A2A gives a client three ways to follow a task: poll it with GetTask, stream it with SendStreamingMessage or SubscribeToTask, or register a webhook and receive push notifications. Polling works with every agent. Streaming and push are optional capabilities that an agent declares in its Agent Card as capabilities.streaming and capabilities.pushNotifications. If a client calls a streaming operation on an agent that doesn’t declare it, the agent must return UnsupportedOperationError (-32004). Push configuration calls on an agent without push support must return PushNotificationNotSupportedError (-32003).
Streams use Server-Sent Events over the JSON-RPC and HTTP+JSON bindings, and server-streaming RPCs over gRPC. Push notifications are HTTP POSTs from the agent to a URL the client registered, which puts security duties on both sides. Emissar’s own public agent supports neither: its card declares both flags false, and its real refusals are at the end of this guide.
Check the card first
| Card flag | If true |
If false or missing |
|---|---|---|
capabilities.streaming |
SendStreamingMessage and SubscribeToTask work |
Both must return -32004 |
capabilities.pushNotifications |
Create, get, list and delete push configs work | All four must return -32003 |
Clients should read these flags before trying either feature (section 3.3.4).
Streaming
Opening a stream
SendStreamingMessage takes the same request as SendMessage and returns a stream instead of one response. Over JSON-RPC the server answers with HTTP 200 and Content-Type: text/event-stream. Each event’s data field is a complete JSON-RPC response whose result is a StreamResponse. Over HTTP+JSON, POST /message:stream returns the same event stream with the bare StreamResponse in each event. Over gRPC it is a server-streaming RPC.
A StreamResponse holds exactly one of four members:
| Member | Contents |
|---|---|
task |
The full Task: id, context, status, artifacts, history |
message |
A Message, for agents that answer without creating a task |
statusUpdate |
taskId, contextId, the new status, optional metadata |
artifactUpdate |
taskId, contextId, an artifact, the flags append and lastChunk, optional metadata |
The member name identifies the event type. Version 0.3 used a kind field for this, and v1.0 removed it.
What arrives, in what order
Section 3.1.2 allows two stream shapes:
- Message-only: exactly one
Message, then the stream closes. - Task lifecycle: the
Taskfirst, then zero or more status and artifact updates, then the stream closes when the task reaches a terminal state.
Events must arrive in the order they were generated, on every binding. An agent may serve several streams for one task. Each stream gets the same events in the same order, and closing one doesn’t affect the others (section 3.5.2).
Large artifacts can arrive in pieces. An artifactUpdate with append: true adds its parts to the artifact with the same artifactId, and lastChunk: true marks the final piece.
Illustrative: a JSON-RPC stream for a freight quote.
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"jsonrpc": "2.0", "id": 1, "result": {"task": {"id": "task-51", "contextId": "ctx-9", "status": {"state": "TASK_STATE_WORKING", "timestamp": "2026-09-26T15:00:00.000Z"}}}}
data: {"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {"taskId": "task-51", "contextId": "ctx-9", "artifact": {"artifactId": "quote-1", "parts": [{"text": "Lane: Toronto to Chicago. "}]}, "append": false, "lastChunk": false}}}
data: {"jsonrpc": "2.0", "id": 1, "result": {"artifactUpdate": {"taskId": "task-51", "contextId": "ctx-9", "artifact": {"artifactId": "quote-1", "parts": [{"text": "Rate valid for 48 hours."}]}, "append": true, "lastChunk": true}}}
data: {"jsonrpc": "2.0", "id": 1, "result": {"statusUpdate": {"taskId": "task-51", "contextId": "ctx-9", "status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-09-26T15:00:04.000Z"}}}}
Version 0.3 marked the last status event with final: true. Version 1.0 removed that field, so the end of the stream is the signal.
When a stream closes
The specification is not consistent here. Sections 3.1.2 and 3.1.6 say a task stream must close when the task reaches a terminal state. Section 11.7, for HTTP+JSON, and the project’s streaming guide say it closes at a terminal or interrupted state, such as TASK_STATE_INPUT_REQUIRED. Section 7.6.1 adds that an agent in TASK_STATE_AUTH_REQUIRED should keep active streams open while it waits for an out-of-band credential.
Write clients for both. If a stream ends and the task is not terminal, check its state with GetTask. For input, send the answer with SendStreamingMessage on the same taskId. For an out-of-band credential, call SubscribeToTask to watch for the resumption. The multi-turn guide covers both interrupted states.
Reconnecting
The A2A specification defines no SSE event IDs and no replay through the Last-Event-ID header. To resume after a dropped connection, open a new stream with SubscribeToTask, which replaced v0.3’s tasks/resubscribe. Its first event must be the current Task, so nothing falls into a gap between reading the task and subscribing (section 3.1.6). Subscribing to a terminal task returns -32004, and the stream closes when the task ends.
What you missed while disconnected is not replayed. Section 3.7 says a client that reconnects may not receive every status message, and that messages must not be treated as reliable delivery for critical information. Rebuild your view from the snapshot: the task’s current status and its artifacts.
One practical limit applies to browsers. The HTML standard’s EventSource constructor accepts only a URL and a withCredentials option, so it cannot send a request body or custom headers. The streaming operations need a request body and the A2A-Version header, so a browser client has to read the response with a general HTTP client and parse text/event-stream itself.
Push notifications
Registering a webhook
A client registers a webhook in one of two ways:
- In the
SendMessageorSendStreamingMessagerequest, asconfiguration.taskPushNotificationConfig, with the task ID left empty. - For an existing task, with
CreateTaskPushNotificationConfig. Get, list and delete operations manage configs afterwards. Delete is idempotent.
| Field | Meaning |
|---|---|
url |
Required. The webhook the agent will POST to |
token |
Optional value unique to this task or session, for the client to check |
authentication |
scheme (such as Bearer) and credentials the agent must send |
id |
Config ID, assigned by the agent |
taskId |
The task this config belongs to |
tenant |
Routing value, if the chosen interface declares one |
A config lasts until the task completes or the client deletes it. Illustrative JSON-RPC request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "CreateTaskPushNotificationConfig",
"params": {
"taskId": "task-51",
"url": "https://client.example.com/a2a/webhook",
"token": "c7a1-task-51",
"authentication": { "scheme": "Bearer", "credentials": "whk-5f2e9a" }
}
}
Delivery
When the task changes, the agent POSTs a StreamResponse to the URL: the same four event types a stream carries. It sends Content-Type: application/a2a+json and an Authorization header built from the config’s authentication. Webhook calls always use plain HTTP and the HTTP+JSON payload shapes, whatever binding created the task (section 3.5.1).
Illustrative webhook call:
POST /a2a/webhook HTTP/1.1
Host: client.example.com
Authorization: Bearer whk-5f2e9a
Content-Type: application/a2a+json
{"statusUpdate": {"taskId": "task-51", "contextId": "ctx-9", "status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-09-26T18:30:00.000Z"}}}
| The agent | The client’s webhook |
|---|---|
| Must attempt delivery at least once per config | Must answer with a 2xx status |
| May retry with exponential backoff | Should process notifications idempotently, since duplicates happen |
| Should time out each request after 10 to 30 seconds | Must check the task ID matches a task it expects |
| May stop after a number of consecutive failures | Should verify where the notification came from |
The streaming guide describes the usual next step: verify the notification, then call GetTask for the full task and any new artifacts.
Securing the agent side
The agent is the side that receives a URL from a stranger and then makes requests to it. That is the setup for server-side request forgery (SSRF): a client could register http://127.0.0.1/admin or a cloud metadata address and use the agent to reach internal systems, or aim many agents at one victim. Section 13.2 says agents should validate webhook URLs:
- Reject private IPv4 ranges:
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12and192.168.0.0/16. - Reject localhost and link-local addresses.
- Use allowlists where they fit.
The streaming guide adds ownership verification, such as a challenge-response, and egress firewalls. OWASP’s SSRF cheat sheet adds checks that close common bypasses: resolve the hostname and test every A and AAAA record, IPv6 included, against the same rules, and turn off redirect following in the HTTP client that makes the call.
The agent must send the credentials the config specifies, should store configs and credentials securely, and should use HTTPS webhook URLs.
Securing the receiving side
The webhook must verify that each notification came from the agent. Section 13.2 also says it should check the task ID, rate-limit incoming calls, use HTTPS, and use a unique, single-purpose token per config, treated as a secret and rotated. The streaming guide suggests timestamps to reject old notifications and unique IDs, such as a JWT jti, to reject replays. It also describes an asymmetric option: the agent signs a JWT per notification and publishes its public keys at a JWKS endpoint, and the webhook verifies the signature and the iss, aud, iat, exp and jti claims.
Polling, streaming or push
| Polling | Streaming | Push | |
|---|---|---|---|
| Operations | GetTask |
SendStreamingMessage, SubscribeToTask |
Push config operations plus a webhook |
| Card flag needed | None | streaming |
pushNotifications |
| Connection | Short requests | One long-lived response per stream | None held open |
| Client must be reachable | No | No | Yes, at a public HTTPS URL |
| Latency | Your polling interval | Immediate | Immediate, subject to retries |
| Fits | Simple clients, rare updates, restrictive networks | Interactive clients, live progress, incremental artifacts | Server-to-server work, tasks that run for hours or days |
Two settings interact with this choice. SendMessage blocks by default until the task is terminal or interrupted. With configuration.returnImmediately: true it returns at once, and the client then polls, subscribes or waits for a webhook. That flag has no effect on streaming calls or on push delivery (section 3.2.2).
Emissar’s agent: no streaming, no push
Emissar’s public agent at https://emissar.ai/a2a/v1 declares streaming: false and pushNotifications: false. Its tasks finish or pause inside the SendMessage call, and a client that needs to check later polls with GetTask. These are its real responses on 2026-09-26.
SendStreamingMessage with a text part (messageId smoke-test-docs-stream-1), returned as a plain JSON response with HTTP status 200:
{
"jsonrpc": "2.0",
"id": 6,
"error": {
"code": -32004,
"message": "Streaming is not supported by this agent (capabilities.streaming is false). Use SendMessage.",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "UNSUPPORTED_OPERATION",
"domain": "a2a-protocol.org"
}
]
}
}
CreateTaskPushNotificationConfig with a webhook URL:
{
"jsonrpc": "2.0",
"id": 7,
"error": {
"code": -32003,
"message": "Push notifications are not supported by this agent.",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "PUSH_NOTIFICATION_NOT_SUPPORTED",
"domain": "a2a-protocol.org"
}
]
}
}
Both follow section 3.3.4: -32004 for streaming and -32003 for push when the card doesn’t declare the capability.
Questions
- Does an A2A stream close when the task needs input?
- The specification's operation sections say a task stream closes at a terminal state. Its HTTP+JSON streaming section and the streaming guide say it closes at a terminal or interrupted state. Clients should handle both: if the stream ends while the task waits for input, answer with a new message on the same taskId.
- Can a browser use EventSource to read an A2A stream?
- Not for the standard operations. The EventSource constructor accepts only a URL and a withCredentials option, so it cannot send a request body or an A2A-Version header, and SendStreamingMessage needs both. Use an HTTP client that reads the response body and parses text/event-stream itself.
- Do push notifications replace GetTask?
- No. Notifications can arrive more than once and a client may miss one. The streaming guide's usual pattern is to verify the notification and then call GetTask for the complete, current task.
Sources
- A2A Protocol Specification (sections 3.1, 3.2.2, 3.3.4, 3.5, 3.7, 4.3, 9.4, 11.7 and 13.2) (accessed )
- A2A protocol definition (a2a.proto): StreamResponse, update events, TaskPushNotificationConfig (accessed )
- Streaming and asynchronous operations (A2A documentation) (accessed )
- What's new in A2A v1.0 (A2A documentation) (accessed )
- HTML Living Standard, section 9.2: Server-sent events (accessed )
- OWASP Server-Side Request Forgery Prevention Cheat Sheet (accessed )
- Emissar Agent Card (live example agent) (accessed )