Learn

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 Task first, 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 SendMessage or SendStreamingMessage request, as configuration.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/12 and 192.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

  1. 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 )
  2. A2A protocol definition (a2a.proto): StreamResponse, update events, TaskPushNotificationConfig (accessed )
  3. Streaming and asynchronous operations (A2A documentation) (accessed )
  4. What's new in A2A v1.0 (A2A documentation) (accessed )
  5. HTML Living Standard, section 9.2: Server-sent events (accessed )
  6. OWASP Server-Side Request Forgery Prevention Cheat Sheet (accessed )
  7. Emissar Agent Card (live example agent) (accessed )