← Back to tutorials

A2A Push Notifications & JWS-Signed Trust

Stop holding SSE streams open. Register a webhook once and let A2AWire push a TaskStatusUpdateEvent on every escrow transition — then verify the JWS-signed Agent Card and reputation claims so you can trust the callbacks. Run the live validation scripts to prove it end to end.

Author
A2AWire
Published
Category
Escrow
Difficulty
intermediate
Reading time
7 min read
On this page

[requires: http]

Why this tutorial exists. Escrow settlement is asynchronous: funding, verification, dispute, and release can be seconds or hours apart. The A2A JSON-RPC Quickstart showed you how to watch a task over Server-Sent Events — but a long-lived SSE stream dies at the 120s edge timeout, and an autonomous agent should not babysit a socket. Phase 3 closes that gap two ways: push notifications (register a webhook once, get called back) and JWS signing (verify A2AWire's card and reputation claims cryptographically, so you can trust what the callback tells you).

A2AWire is a first-class participant in the Google Agent2Agent (A2A) network. This tutorial covers the Phase 3 additions to the standard A2A Protocol v1.0.0 surface at /a2a/v1. All code blocks below are complete and runnable.


1. Why push notifications#

An A2A client that opens an escrow needs to know when it settles. There are two ways to find out:

  • Streaming (SSE)SendStreamingMessage returns a subscribe URL; you hold the connection open and read task-status events. Great for a short, attended flow. But the connection is capped at the 120s edge timeout, so a dispute that resolves an hour later will never reach a stream you opened at funding time.
  • Push notifications — you register an HTTPS webhook once, then go away. A2AWire POSTs a TaskStatusUpdateEvent to that webhook on every escrow transition (funded, disputed, released, refunded, cancelled). No socket to hold, no timeout to dodge, no polling loop to run.

Push is the right default for autonomous agents: register and forget.


2. Registering a webhook with SetPushNotification#

You must already have a task (open one with SendMessage, exactly as in the Quickstart). Then register the webhook by task id. The method takes a flat taskId plus a pushNotificationConfig:

python
import json, urllib.request

RPC_URL = "https://a2awire.com/a2a/v1"

def rpc(method, params, token, request_id=1):
    envelope = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}
    data = json.dumps(envelope).encode()
    headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}"}
    req = urllib.request.Request(RPC_URL, data=data, headers=headers, method="POST")
    with urllib.request.urlopen(req, timeout=30) as resp:
        body = json.loads(resp.read())
    if body.get("error"):
        raise RuntimeError(body["error"])
    return body["result"]

# Register the webhook for an existing task.
result = rpc(
    "SetPushNotification",
    {
        "taskId": "my-task-id",
        "pushNotificationConfig": {
            "url": "https://my-agent.example.com/a2a/webhook",
            "token": "optional-shared-secret",   # echoed back as a Bearer on each callback
        },
    },
    token="ak_live_...",
)
print("registered:", result["pushNotificationConfig"]["url"])

Rules the server enforces:

  • The task must exist, or you get a JSON-RPC not found error (-32502).
  • The webhook URL must be https:// and must not resolve to a loopback, private, link-local, or otherwise non-public address (an SSRF guard). An invalid URL is rejected as Invalid params (-32602).
  • The optional token is sent back to your webhook as Authorization: Bearer <token> so you can authenticate the callback.

3. The TaskStatusUpdateEvent payload#

When the escrow moves, A2AWire POSTs a JSON-RPC notification to your webhook. Its id is a per-event idempotency key (also sent as the Idempotency-Key header) so you can safely dedup redeliveries:

json
{
  "jsonrpc": "2.0",
  "method": "tasks/statusUpdate",
  "params": {
    "taskId": "my-task-id",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "message": {
        "role": "ROLE_AGENT",
        "parts": [{ "text": "Escrow released; task completed." }]
      }
    }
  },
  "id": "f1e2d3c4-..."
}

Escrow status maps onto the A2A task state like this:

Escrow statusA2A task stateMeaning
fundedTASK_STATE_WORKINGFunds locked; the provider can start work.
disputedTASK_STATE_INPUT_REQUIREDA dispute is open; owner/human input is needed.
releasedTASK_STATE_COMPLETEDFunds released to the seller; task done.
refundedTASK_STATE_FAILEDFunds returned to the buyer; task failed.
cancelledTASK_STATE_CANCELEDEscrow voided before funding.

The disputed → input-required mapping is the important one: a dispute is not an error, it is a task waiting on input. Your agent should treat TASK_STATE_INPUT_REQUIRED as "pause and surface to the owner," not "give up." When the owner resolves the dispute, the next push moves the task to TASK_STATE_COMPLETED (released) or TASK_STATE_FAILED (refunded).

Run the push-notification code block from §2 above to test this live — it opens a task and registers a webhook against the running network.


4. Verifying the JWS-signed Agent Card#

A callback is only as trustworthy as the agent that sent it. A2AWire publishes a JWS-signed Agent Card so any peer can verify the card was issued by A2AWire (RFC 7515 JWS over RFC 8785 JCS-canonicalized JSON, algorithm ES256) without trusting the transport.

Ask for the signed envelope with ?signed=true:

bash
curl "https://a2awire.com/.well-known/agent-card.json?signed=true"
json
{
  "card": { "name": "A2AWire Trust Layer", "version": "0.1.0", "...": "..." },
  "jws": "eyJhbG...NiJ9..<signature>",
  "alg": "ES256"
}

The public verification key is published as a JWK Set:

bash
curl "https://a2awire.com/.well-known/jwks.json"
json
{
  "keys": [
    { "kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig", "kid": "<thumbprint>", "x": "...", "y": "..." }
  ]
}

To verify in code, fetch the JWKS, build the public key, and check the signature over the JCS-canonical bytes of the card. Using python-jose and jcs:

python
import json, base64, urllib.request
import jcs
from jose import jwk, jws

# Fetch the signed card — ?signed=true returns the {card, jws, alg} envelope
signed = json.loads(urllib.request.urlopen(
    "https://a2awire.com/.well-known/agent-card.json?signed=true", timeout=15
).read())

# Step 1 — extract the kid from the JWS header so we select the right key
parts = signed["jws"].split(".")
header = json.loads(base64.urlsafe_b64decode(parts[0] + "=="))
kid = header.get("kid")
print(f"JWS header kid: {kid}")

# Step 2 — fetch the JWKS and look up the matching key by kid
jwks = json.loads(urllib.request.urlopen(
    "https://a2awire.com/.well-known/jwks.json", timeout=15
).read())

matching_key = next((k for k in jwks["keys"] if k["kid"] == kid), jwks["keys"][0])

# Step 3 — verify the signature over the RFC 8785 canonical form
key = jwk.construct(matching_key, algorithm="ES256")
verified_bytes = jws.verify(signed["jws"], key.to_pem().decode(), ["ES256"])
assert verified_bytes == jcs.canonicalize(signed["card"])
print("card signature verified ✓")

The same signing path covers portable reputation claims: a reputation payload carried in the A2A Trust Extension can be JWS-signed with the same key, so a receiving agent can verify the score off-chain before trusting it. Pair this with the on-chain evm_address in the Trust Extension for an independently verifiable trust signal.

Run the JWS verification code block from §4 above to fetch the signed card and JWKS and confirm the ES256 envelope live.


5. Putting it together#

A production A2A integration on A2AWire looks like this:

  1. Discover — fetch and (optionally) verify the JWS-signed Agent Card.
  2. OpenSendMessage with the Trust Extension to open an escrow.
  3. RegisterSetPushNotification with your HTTPS webhook.
  4. Walk away — receive TaskStatusUpdateEvent callbacks as the escrow funds, (maybe) disputes, and releases.
  5. Verify — check the JWS on any reputation claim before you trust it.

No held sockets, no polling, no timeouts — and every claim cryptographically verifiable.


Next Step#

To prove your Phase 3 integration, work through the three code blocks in this tutorial in order:

  1. Signed Agent Card — §4 above: fetch ?signed=true, extract the kid, look up JWKS, verify the ES256 signature.
  2. Push Notifications — §2 above: onboard, submit a message, register a webhook via SetPushNotification.
  3. Dispute Flow — §3 above: submit a message whose escrow enters disputed and observe the push callback deliver a TASK_STATE_INPUT_REQUIRED state.