The Agent2Agent protocol solved a real problem. Long-running agent tasks do not fit inside a request and response, and holding an SSE stream open for forty minutes is not something you want to depend on. So A2A lets the client register a webhook and disconnect, and the remote agent posts back when something worth knowing about happens.
The specification is careful about the parts it covers. It is also explicit, by omission, about a part it does not cover, and that part is the one that decides whether your integration works at three in the morning.
What the spec actually gives you
A push notification is configured with a PushNotificationConfig. The fields are small and sensible:
-
url, the HTTPS endpoint that will receive the POST -
token, an optional opaque value the receiver can check to confirm the notification belongs to a task it knows about -
authentication, optional details describing how the sending agent will authenticate itself to your webhook You can supply it inline on the initialSendMessageorSendStreamingMessagecall, or manage it separately for an existing task. The current specification exposes create, get, list, and delete operations for these configs, so a client can rotate an endpoint without restarting the task.
On the wire, the notification body uses the same StreamResponse shape as streaming, carrying exactly one of a task, a message, a statusUpdate, or an artifactUpdate. If you have already written a streaming consumer, you have most of a webhook consumer.
The spec also tells you when to expect one. The server decides, but the guidance is that notifications fire on significant state changes. A task is in one of eight states. Four are terminal: COMPLETED, FAILED, CANCELED, REJECTED. Four are interim: SUBMITTED, WORKING, INPUT_REQUIRED, AUTH_REQUIRED. In practice you will be notified on the terminal ones, and on INPUT_REQUIRED and AUTH_REQUIRED, because those are the two interim states where the task has stopped and is waiting on you.
On authentication it is genuinely helpful. Bearer tokens, API keys, HMAC signatures, and mTLS are all named, with a JWT and JWKS example for key distribution. The security guidance is good in both directions. The sending agent should treat a client-supplied URL as hostile and defend against SSRF with domain allowlisting, ownership verification, and egress controls. The receiving webhook should verify signatures against trusted keys, validate the token if one was set, and use timestamps and nonces or jti claims to reject replays.
That is a well-specified protocol. Read it and you know exactly what a notification looks like and how to prove it came from who it claims to.
The sentence that is not in the specification
Nowhere does A2A say what happens if your webhook is down.
There is no retry policy. No backoff guidance. No timeout threshold. No statement about how many attempts a sending agent should make, or whether it should make any. No definition of at-least-once or at-most-once. The spec says notifications are delivered by HTTP POST and leaves everything after that word to the implementation.
This is a defensible choice. Protocols that try to mandate delivery semantics tend to age badly. But it means something specific for anyone building on A2A: the reliability of your agent notifications is not a property of the protocol. It is a property of whichever implementation happens to be on the other end, and you probably have not read its source.
Two A2A-compliant agents can behave completely differently here. One retries ten times over six hours with exponential backoff. One tries once, catches the exception, logs it, and moves on. Both are conformant. Your integration passes its tests against either.
Why this bites harder for agents than for ordinary webhooks
A dropped order.paid webhook is bad. A dropped A2A terminal notification is worse, and the reason is structural.
Push notifications exist precisely for the long-running case. The client disconnected on purpose. That is the feature. So when the agent finishes a task that took forty minutes of real compute, there is often exactly one POST that says so, and nobody is watching the connection anymore.
If that POST fails and is not retried, three things are true at once. The work happened. The result exists on the remote agent. And your side of the system believes the task is still WORKING, forever, because the notification that would have moved it was the notification that got lost.
With streaming you would have noticed the disconnect. That is the trade you made when you chose push. It is a good trade, but it moves the burden of noticing onto the delivery layer.
There is a second-order version of this that is nastier. Because the sender chooses when to notify, an agent can legitimately send you the same state transition twice, and a retrying sender certainly will. If your webhook handler is not idempotent, a duplicate COMPLETED notification does not just log twice. It runs whatever your completion handler runs, twice.
What the receiving side owes
Your webhook is a public HTTPS endpoint that a third party posts to. Treat it that way.
Verify before you trust. Check the signature or bearer token against the key you expect, compare the token field against the value you registered, and do both with constant-time comparison so you are not leaking the credential a byte at a time through timing. Return 401 on a mismatch and do not explain which part failed.
Reject replays. A timestamp window plus a seen-nonce set is enough. The jti claim exists for this if you are on JWTs.
Be idempotent, and key it correctly. The natural key for an A2A notification is the task ID combined with the state being reported, not a random message ID, because the thing you want to happen only once is "this task became COMPLETED", regardless of how many POSTs carry that news.
Return quickly. Acknowledge with a 2xx as soon as the notification is durably written on your side, then do the actual work asynchronously. A handler that takes eleven seconds because it calls three internal services will eventually take longer than the sender's timeout, and then you are being retried for work you already completed.
What the sending side owes
If you are the one running the agent, the notification is your outbound delivery problem, and it looks exactly like every other outbound delivery problem.
Write it down before you send it. The state transition should be durable in your own database before the POST is attempted, because a process that crashes between "task completed" and "notification sent" otherwise loses the only record that a notification was owed.
Retry, but retry with judgement. A connection timeout or a 503 deserves another attempt on a backoff schedule with jitter, because the endpoint is probably coming back. A 422 or a 400 does not, because the payload will be equally wrong in ten minutes and all you are doing is delaying the moment somebody discovers the integration has been broken since it shipped. A 410 Gone is the receiver telling you to stop, and the right response is to disable the config rather than to keep trying.
Fail somewhere visible. When the retry budget is exhausted, the notification should land in a dead letter queue that a person actually reads, stored whole rather than as a log line, so that when the endpoint comes back somebody can replay it. Alert on the endpoint rather than on the individual notification, or you will teach everyone to ignore the alert.
Defend the URL. The client hands you an arbitrary HTTPS address and asks you to POST to it from inside your network. That is a textbook SSRF vector, which is why the spec calls it out. Allowlist, verify ownership, and keep egress controlled.
Build or buy, honestly
None of the above is exotic. A competent backend team can build a persisted queue, a retry worker with jitter, a dead letter table, and a replay endpoint. Teams do it every quarter.
The cost is not the build. It is that you have now taken ownership of a piece of infrastructure whose failure mode is silence, and it has to keep working correctly while you build the agent your users are actually paying for. Delivery infrastructure does not page you when it breaks. It just stops mentioning things.
If you are running one agent, notifying one endpoint your own team operates, write the retry loop. That is the correct amount of engineering for that problem. The line worth watching is not volume. It is who owns the other end. Once you are posting to endpoints operated by other people, on their uptime and their deploy schedule, the failure modes stop being yours and you find out about them from a customer.
The short version
A2A tells you what a push notification looks like and how to prove where it came from. It does not tell you what happens when the POST fails, and that gap is yours to fill on both sides of the connection.
Persist the state transition before you send it. Retry the retryable and stop fast on everything else. Make the receiver idempotent on task ID plus state, because duplicates are not a bug in the sender, they are the sender doing its job. Verify credentials in constant time and reject replays. And put the notifications that never made it somewhere a person will look, because a task that silently stays WORKING forever is the one failure mode nobody has a dashboard for.
Mittr is the reliable action layer for webhooks and AI agent actions, including A2A push notifications. It handles both auth presets from the spec, bearer token and OIDC or OAuth2 JWT, with constant-time credential checks and a 401 on mismatch. Every receipt is recorded in a request inspector, idempotency keys are derived from task plus state, and notifications are correlated by task ID. Outbound, every action is written to Postgres before delivery, retried on a front-loaded schedule with jitter behind a per-endpoint circuit breaker, and dead-lettered rather than dropped if it exhausts its budget. The free tier is 3,000 messages a month, no card. More at mittr.io.
Top comments (0)