AAC Docs

A2A integration

AAC Sidecar v0.5.1MarkdownDocs b7e0bf78604d

Agent-to-Agent (A2A) is an open protocol for one agent to send a message to another over HTTPS, using JSON-RPC 2.0 and a published Agent Card that says how to reach the agent. AAC supports A2A: the sidecar beside your agent receives A2A messages from other organizations' agents, verifies the sender's delegated authority before your handler sees the message, and sends your agent's A2A messages to configured peers under authority it mints and signs.

This page is the home for A2A on this site. It explains what is supported, walks through enabling A2A for one agent and sending a first message, and documents the a2a configuration block, the dispatch envelope, the Agent Card and the A2A errors and audit events.

What AAC adds to A2A

A plain A2A call proves nothing about who authorized it. With AAC, every A2A request between two sidecars carries a delegated authority chain and a proof-of-possession signature. The receiving sidecar checks both against the public trust material of the sender's tenant, confirms that its own agent is the intended recipient, and only then hands the unchanged A2A message to your handler together with verified context: which tenant started the task, which workload is presenting, and the task reference. Your handler never sees or handles the credentials.

Choose A2A when the other agent speaks A2A, or when your agent already exposes an A2A handler. Choose native AAC delivery when both sides are AAC sidecars and you want the forward, settle and refuse decision flow with signed terminal receipts. One sidecar can do both; the native example on the integration page runs the two side by side.

What is supported

Surface Supported profile
Protocol A2A 1.0 over JSON-RPC 2.0; exactly one A2A-Version: 1.0 header; no extensions
Method Unary SendMessage only
Message parts text, and data with mediaType: application/json; acceptedOutputModes limited to text/plain and application/json
Reply One message from the agent, or one task in a terminal state
Discovery GET /.well-known/agent-card.json on the sidecar's external listener
Sending Your paired agent asks its own sidecar to dispatch to a configured destination; the sidecar never fetches a peer's card or follows a redirect
Retries A dispatch has an identifier; an identical retry returns the retained outcome and never repeats the operation
Not supported Streaming, server-sent events, task lifecycle and multi-turn tasks, push notifications, extended cards, file, raw or URL parts, gRPC, HTTP+JSON binding, A2A 0.3 compatibility

An unsupported method is answered with a JSON-RPC error, not with a fallback.

How a message travels

Receiving, from a peer's sidecar to your handler:

peer sidecar ──HTTPS: POST /a2a/v1──▶ your sidecar (external listener)
                                     verifies the AAC authority and DPoP proof,
                                     checks you are the intended recipient,
                                     admits the body within configured limits
                                   ──HTTP: POST /a2a/v1──▶ your handler
                                     unchanged A2A body + verified X-AAC-* headers,
                                     signed with the pairing secret
                                   ◀── your JSON-RPC reply ──
peer sidecar ◀── the same reply, after the sidecar checks it fits the profile

Sending, from your agent to a peer:

your agent ──HTTP: POST /v1/agent/a2a/dispatch──▶ your sidecar (loopback listener)
            signed envelope naming a destination,     mints the authority,
            an authority mode and the A2A request     claims the dispatch id
                                                 ──HTTPS: POST /a2a/v1──▶ peer sidecar
                                                 ◀── peer's reply ──
your agent ◀── {"dispatch_id": "…", "status": "dispatched"} ──

The sidecar validates the peer's reply against the profile and retains the outcome for the retry window. It returns an acknowledgement to your agent, not the peer's reply content. Treat a dispatch as a one-way message with confirmation of delivery; if your workflow needs the peer's answer, the peer's agent sends it as its own message to you.

Set up A2A for your agent

You need a registered tenant and agent and a sidecar running beside the agent. If you have neither, start the AAC journey first; the CLI guide covers tenant and agent registration. The steps below add A2A to an agent prepared with the CLI's init command.

1. Enable A2A in the configuration

Add an a2a section to the agent's configuration file, the YAML you pass to the init command's --agent-config option. Two values are yours to choose: the HTTPS address at which peers reach your sidecar, and the local address of your handler.

a2a:
  public_base_url: "https://orders.example.com"
  local_handler_url: "http://127.0.0.1:8000/a2a/v1"

Then run the same init command you used for the agent, for example:

aac init --profile stage --agent orders --agent-config orders.yaml

The CLI writes a complete a2a block into the agent's sidecar configuration with the request limits, a 60-second deadline, and both sending blocks (continuation_authority and egress_idempotency) with managed state paths. The public address must be https:// with no path; the handler address must end in exactly /a2a/v1. The field table on the CLI package page lists the accepted values. Restart the sidecar after the command finishes.

If you maintain the sidecar YAML yourself, copy the a2a section of the configuration template and set every value; the a2a settings below give each bound. An agent that only receives A2A messages can leave out both sending blocks; see receive A2A messages without sending. In either form, sidecar.agent_invoke_auth.secret_file is required whenever an a2a block is present, because the sidecar signs every call to your handler.

2. Write the A2A handler

Your agent serves POST /a2a/v1 on the local handler address. The sidecar calls it with the peer's A2A request body unchanged and these verified headers, all covered by the pairing signature:

Header Meaning
X-AAC-Context-Schema Always aac.a2a.ingress-context.v1
X-AAC-Task-Ref The task the sender's authority is restricted to
X-AAC-Presenter-Spiffe-Id The verified workload identity that sent the message
X-AAC-Originator-Tenant-Id The tenant whose agent started the task
X-AAC-Root-Token-Id, X-AAC-Presenter-Token-Id, X-AAC-Hop-Index Correlation with the authority chain; the presenter token id is also the handle for continuing the authority when you send onward
X-AAC-Invoke-Timestamp, X-AAC-Invoke-Signature The pairing signature your handler must verify before trusting any of the above

Verify the pairing signature first, so that only your sidecar can reach the handler. Python agents can use the published aac-invoke-auth package, as this example does; other languages implement the pairing protocol. Save this as a2a_agent.py:

from fastapi import FastAPI, Request
from aac_invoke_auth.fastapi import InvokeAuthGuard, InvokeAuthMiddleware

app = FastAPI()
app.add_middleware(
    InvokeAuthMiddleware,
    guard=InvokeAuthGuard.from_env(),
    protected_paths=("/a2a/v1",),
)


@app.post("/a2a/v1")
async def a2a(request: Request):
    body = await request.json()
    message = body["params"]["message"]
    # The sidecar verified these before calling; they are context, not credentials.
    sender = request.headers["X-AAC-Presenter-Spiffe-Id"]
    task = request.headers["X-AAC-Task-Ref"]
    text = " ".join(part.get("text", "") for part in message["parts"])
    return {
        "jsonrpc": "2.0",
        "id": body["id"],
        "result": {
            "message": {
                "messageId": message["messageId"] + "-reply",
                "contextId": task,
                "role": "ROLE_AGENT",
                "parts": [{"text": f"Received {len(text)} characters from {sender} for {task}"}],
            }
        },
    }

The handler and the send script in step 4 run in a Python environment of your own on the agent's host. Installing the CLI does not provide their packages, so create the environment once and install them:

python -m venv "$HOME/.aac/a2a-env"
source "$HOME/.aac/a2a-env/bin/activate"
python -m pip install --upgrade 'aac-invoke-auth[fastapi]' uvicorn httpx

Then start the handler in that environment with the pairing secret the CLI placed in the agent folder, in the same network namespace as the sidecar:

export AAC_INVOKE_AUTH_SECRET_FILE="$HOME/.aac/agents/orders/agent/pairing.secret"
python -m uvicorn a2a_agent:app --host 127.0.0.1 --port 8000

The handler must answer within the configured deadline with HTTP 200, Content-Type: application/json, no compression, and a JSON-RPC 2.0 response that preserves the request id and contains exactly one result or error. A result holds exactly one message (nonempty messageId and contextId, role ROLE_AGENT, at least one text or data part) or one task in a terminal state (id, contextId, status.state). An error has an integer code and a nonempty message. A response outside this profile is refused by the sidecar and reported to the peer as ERR_INVALID_AGENT_RESPONSE. Values shaped like AAC credentials are refused anywhere in the response.

A handler that serves only /a2a/v1 is enough for A2A. The sidecar also has an agent_invoke_url for native delivery; if no native message is ever sent to this agent, that path is never called.

3. Name the destination and the authority

Sending needs two more entries in the agent configuration: a destination that names the peer, and a class of action that describes the authority your agent will mint for the message. Both use the same fields as native delivery.

destinations:
  partner_a2a:
    url: "https://partner.example.net/a2a/v1"
    audience_pattern: "spiffe://partner.example.net/agents/approvals"
    predicates: {action: "request_quote"}
    valid_for: "+5m"
    timeout_ms: 10000
classes_of_action:
  request_quote:
    predicates: {action: "request_quote"}
    valid_for: "+10m"

The destination url is the peer's public base URL plus /a2a/v1, the address its Agent Card advertises, and audience_pattern is the peer agent's exact registered SPIFFE ID: the peer's sidecar refuses a message whose audience is not its own identity. Predicates come from the registered vocabulary; agree their business meaning with the peer. When sending is enabled, every destination's timeout_ms must be positive and no greater than the A2A deadline. Run the init command again to apply the change, then restart the sidecar.

Your sidecar must also trust the peer, and the peer must trust you: each receiver lists the sender's tenant under trust_anchors.tenant_ids and the sender's trust domain under spiffe_bundles.trust_domains, and both tenants publish their trust material with the trust anchor publisher. Two agents across tenants lists everything the two sides exchange.

4. Send a message

Your agent sends by posting a signed envelope to its own sidecar's loopback listener. The envelope names the destination, states how the sidecar should obtain authority, and carries the A2A request. Save this as a2a_send.py:

import json
import os
import sys
import time
import uuid
from pathlib import Path

import httpx
from aac_invoke_auth import sign_invoke_request

DISPATCH_PATH = "/v1/agent/a2a/dispatch"


def send_message(client, secret, destination_profile, class_of_action, human_originator, text):
    task_ref = "a2a-" + str(uuid.uuid4())
    envelope = {
        "schema_version": "aac.a2a.egress.v1",
        "dispatch_id": str(uuid.uuid4()),
        "destination_profile": destination_profile,
        "task_ref": task_ref,
        "authority": {
            "mode": "originate",
            "class_of_action": class_of_action,
            "human_originator": human_originator,
        },
        "additional_predicates": {},
        "a2a_request": {
            "jsonrpc": "2.0",
            "id": task_ref,
            "method": "SendMessage",
            "params": {
                "message": {
                    "messageId": str(uuid.uuid4()),
                    "role": "ROLE_USER",
                    "parts": [{"text": text}],
                }
            },
        },
    }
    body = json.dumps(envelope, separators=(",", ":")).encode()

    def post():
        headers = {"Content-Type": "application/json", "X-AAC-Envelope-Schema": "aac.a2a.egress.v1"}
        headers.update(
            sign_invoke_request(secret=secret, method="POST", path=DISPATCH_PATH, headers=headers, body=body)
        )
        return client.post(DISPATCH_PATH, headers=headers, content=body)

    response = post()
    if response.status_code != 200:
        raise RuntimeError(f"dispatch refused: HTTP {response.status_code} {response.text}")
    # A retry keeps the dispatch_id and the exact bytes; the sidecar answers from
    # the retained outcome instead of sending the message again.
    retry = post()
    if retry.content != response.content:
        raise RuntimeError("an identical retry returned a different outcome")
    return response.json()


if __name__ == "__main__":
    secret = Path(os.environ["AAC_INVOKE_AUTH_SECRET_FILE"]).read_bytes().strip()
    # A real agent takes these claims from the signed-in user's session.
    human = {"iss": "https://synthetic.invalid", "sub": "demo-only", "auth_time_unix_seconds": int(time.time())}
    with httpx.Client(base_url="http://127.0.0.1:8080", timeout=65, trust_env=False) as client:
        result = send_message(
            client,
            secret,
            os.environ.get("AAC_A2A_DESTINATION", "partner_a2a"),
            os.environ.get("AAC_A2A_CLASS", "request_quote"),
            human,
            " ".join(sys.argv[1:]) or "Hello from an AAC-enabled agent",
        )
        print(json.dumps(result, indent=2))

In a second terminal with the same environment active:

source "$HOME/.aac/a2a-env/bin/activate"
export AAC_INVOKE_AUTH_SECRET_FILE="$HOME/.aac/agents/orders/agent/pairing.secret"
python a2a_send.py "Please quote two seats to Lisbon"

The human_originator claims identify the person on whose behalf the agent acts. The example uses synthetic values; a real agent fills them from its own authenticated session, and AAC does not turn them into an authentication.

5. What success looks like

A delivered message prints the acknowledgement:

{
  "dispatch_id": "6f1c0a8e-3b2d-4c7e-9a1f-2d3e4f5a6b7c",
  "status": "dispatched"
}

Behind that line, the peer's sidecar verified your authority, the peer's handler answered, and your sidecar checked the answer against the profile. The second, identical post in the example returned the same bytes without a second delivery. In your sidecar's telemetry sink, the a2a_egress event records result: accepted; on the peer, a2a_ingress records accepted and the handler's log shows one POST /a2a/v1. See audit your workflows for the event fields.

A refusal prints the sidecar's error envelope with a code from the error table below. Three kinds of answer need different handling. A refusal that happened before the sidecar claimed your identifier (a bad pairing signature or schema header, a malformed envelope, an oversized or compressed body, or exhausted retained capacity) leaves nothing behind: nothing was sent, no record exists, and the same identifier and bytes may be retried after you fix the cause or back off. An in-progress answer means your first attempt holds the claim and may be delivering right now, or still after a sidecar restart: back off and retry the same identifier and bytes, and do not conclude that nothing was sent. Every outcome decided after the claim, including a delivered message, a refused destination or class, and an uncertain or timed-out delivery, is retained under that identifier: an identical retry returns the same answer without sending again. For an uncertain or timed-out delivery, reconcile with the peer and issue a new dispatch_id only once you know the operation was not performed. The responses table says which is which.

To exercise both directions on one machine before you have a peer, the integration page's runnable example runs an agent that handles native and A2A calls and a client that sends an A2A message to the agent's own sidecar.

The dispatch envelope

POST /v1/agent/a2a/dispatch exists on the loopback listener only when both sending blocks are configured; a receive-only sidecar answers 404. The request needs Content-Type: application/json, a body within a2a.max_request_body_bytes, the pairing signature headers exactly once each, and exactly one X-AAC-Envelope-Schema: aac.a2a.egress.v1 header, which the signature covers. The body is a JSON object with these fields and no others; only additional_predicates may be omitted:

Field Value
schema_version aac.a2a.egress.v1
dispatch_id A lowercase UUID version 4, generated by your agent for this operation and kept for every retry
destination_profile The name of a configured destination, 1–128 characters of a-z, digits, _, . and -, starting with a letter; never a URL
task_ref 1–256 printable ASCII characters naming the task, with no leading or trailing whitespace; the receiving handler sees it as X-AAC-Task-Ref
authority One of the two modes below
additional_predicates A JSON object, possibly empty, of registered predicate names that narrow the authority; at most 64 keys; a task_ref predicate must equal the envelope's task_ref
a2a_request The JSON-RPC 2.0 SendMessage request itself, within the same body, depth and node limits as inbound requests

Authority modes:

Responses:

HTTP Body Meaning
200 {"dispatch_id": "…", "status": "dispatched"} Delivered; the peer's reply passed the profile check
200, 4xx or 5xx The retained first outcome An identical retry of a dispatch whose outcome was decided after the claim; same status and bytes as the first time
409 ERR_EGRESS_DISPATCH_IN_PROGRESS error envelope Not retained. The first attempt is still running; back off and retry the same identifier and bytes
409 ERR_EGRESS_IDEMPOTENCY_CONFLICT error envelope Not retained. The identifier was reused with a different body; the first claim still governs it, so keep the original bytes or start a new operation
503 ERR_EGRESS_IDEMPOTENCY_SATURATED error envelope Not retained and not claimed: nothing was sent. Retained capacity for the pair is full; back off and retry the same identifier and bytes once entries expire, or raise the capacity settings
401 ERR_INVALID_REQUEST error envelope Before the claim: the pairing signature or the schema header is missing or wrong
413 ERR_REQUEST_TOO_LARGE, 415 ERR_INVALID_REQUEST error envelope Before the claim: the envelope exceeds max_request_body_bytes, is not application/json, or is compressed
400 ERR_INVALID_REQUEST (malformed envelope) error envelope Before the claim: the envelope fails the field rules above
400 ERR_INVALID_REQUEST (predicates) error envelope Retained: additional_predicates do not narrow the destination's authority
404 ERR_DESTINATION_NOT_FOUND, ERR_CLASS_OF_ACTION_NOT_FOUND error envelope Retained: the envelope named something not configured
409 ERR_CONTINUATION_AUTHORITY_UNAVAILABLE error envelope Retained: no live retained authority for this pair, task and presenter
502 ERR_A2A_REMOTE_REJECTED, ERR_INVALID_AGENT_RESPONSE error envelope Retained: the peer answered with a non-200 status, or with a reply outside the profile
502 ERR_A2A_DISPATCH_UNCERTAIN, 504 ERR_A2A_DISPATCH_TIMEOUT error envelope Retained: the outcome is unknown; an identical retry returns this same answer and does not resend. Reconcile with the peer, and issue a new dispatch_id only once you have confirmed the operation was not performed
503 ERR_CONFIG_ERROR error envelope Retained: the named class or destination has invalid settings

A refusal marked "before the claim", and a saturation refusal, sent nothing and left no record, so the same identifier and bytes may be used again once the cause is fixed. An in-progress answer is different: the first claim stands and may still be delivering, so keep the same identifier and bytes and back off. Retained outcomes are kept for egress_idempotency.retention_seconds from the first claim, across sidecar restarts, in the state_file. Retention is a bounded guarantee, not permanent deduplication: after it expires the same identifier is accepted as a new operation.

The a2a configuration block

Every value is explicit; there is no default body size and no unlimited mode. Size the limits for your deployment and test them before use.

Setting Meaning Accepted values
public_base_url The HTTPS origin at which peers reach your sidecar's external listener; printed on the Agent Card as <public_base_url>/a2a/v1 https:// URL with a host and no path other than /, no query, fragment or credentials
local_handler_url Your agent's A2A handler, called with the unchanged verified body http:// or https:// URL whose path is exactly /a2a/v1
max_request_body_bytes Bound on an inbound request body, a dispatch envelope and the replies the sidecar reads from your handler and from peers Positive integer; required
deadline_seconds End-to-end budget for an inbound request, from admission to the written response, and for each dispatch; when sending is enabled, every destination timeout_ms must fit within it 1–60
max_concurrent_requests Inbound requests admitted at once; beyond it the sidecar answers 503 ERR_A2A_OVERLOADED Positive integer
max_json_nesting_depth Deepest JSON nesting accepted in any A2A body 1–32
max_json_nodes Most JSON values accepted in any A2A body 1–10000
agent_card Optional description of your agent for the public card See describe your agent on its A2A Agent Card
continuation_authority.retention_seconds How long a verified inbound authority may be continued by a continue dispatch; it can only shorten the chain's own expiry Positive integer; present together with egress_idempotency
egress_idempotency.state_file The retained dispatch-outcome database, opened before either listener binds Absolute path on storage that survives the process or container replacement you rely on; missing parents, an unusable file or a second owner stop startup
egress_idempotency.retention_seconds How long a dispatch outcome supports safe retries 120–86400, and at least twice deadline_seconds
egress_idempotency.max_entries_per_pair Live dispatch identifiers retained for this agent pair; saturation refuses new dispatches rather than evicting live ones Positive integer
egress_idempotency.max_cached_response_body_bytes Largest outcome body retained for a dispatch; a larger one is replaced by ERR_EGRESS_RESPONSE_TOO_LARGE At least 203
egress_idempotency.max_reserved_cached_bytes_per_pair Total outcome bytes reserved for the pair At least max_cached_response_body_bytes

Supply both sending blocks or neither; one without the other stops startup. Without them the sidecar is receive-only. Size max_entries_per_pair for peak new dispatches per second multiplied by retention_seconds, then reserve that count multiplied by max_cached_response_body_bytes, plus storage overhead; the template's values suit a small test, not a busy agent. Local or remote redirects are never followed, so every URL must name its final endpoint. The configuration reference explains how the A2A deadline interacts with the other timeouts.

Agent Card and discovery

When an a2a block is present, the sidecar serves your agent's card at GET /.well-known/agent-card.json on its external listener, so a peer that knows your public base URL can discover how to call you. The card advertises:

Check it from any machine that can reach the listener:

curl --fail --silent --show-error https://orders.example.com/.well-known/agent-card.json

Discovery is one-directional. Your sidecar publishes a card, but when sending it never fetches the peer's card: it delivers to the configured destination URL and audience, and refuses redirects. Read a peer's card to learn their address and skills, then write what you learned into your destination entry.

Two agents across tenants

Each side is an independent tenant with its own CA, agents and trust publication. Exchange the following with the peer organization before the first message; nothing private crosses the boundary.

You give the peer The peer gives you
Your tenant ID and trust domain, so their sidecar can list them under trust_anchors.tenant_ids and spiffe_bundles.trust_domains Their tenant ID and trust domain, for your sidecar's two lists
Your sidecar's public_base_url, or simply your Agent Card address Their public base URL, which becomes your destination url with /a2a/v1 appended
Your agent's exact SPIFFE ID, which they set as audience_pattern Their agent's exact SPIFFE ID, for your audience_pattern
The predicates and values you will send, and what they mean The predicates they accept, and the skills their card lists

Both tenants publish their root keys and CA certificates with the trust anchor publisher; a sidecar polls the public trust material of every tenant it lists and refuses a message from a tenant or domain it does not trust. Each side's ingress must allow the other's sidecar to reach the external listener over HTTPS at the final URL, with no redirect. Then:

  1. Peer A adds the destination and class of action for B to its agent configuration and runs the init command; both restart their sidecars.
  2. A's agent sends one message with a2a_send.py; it prints dispatched.
  3. B's handler log shows one POST /a2a/v1 carrying A's tenant and agent in the X-AAC-Originator-Tenant-Id and X-AAC-Presenter-Spiffe-Id headers; B's a2a_ingress event records accepted.
  4. B answers A the same way, with its own destination for A and its own class of action, or continues A's authority with a continue dispatch inside the retention window.

A refusal at step 2 or 3 names its cause: ERR_UNKNOWN_TENANT_KEY or ERR_DPOP_CHAIN on B means B does not yet trust A's published material; ERR_RECIPIENT_NOT_AUTHORIZED means A's audience_pattern is not B's exact SPIFFE ID; ERR_A2A_REMOTE_REJECTED on A reports B's refusal. For a same-tenant run of two agents on one machine, the integration page's two-agent example sets up both processes and sends an A2A message between them.

Errors and audit events

The sidecar writes two A2A event types to its telemetry sink: a2a_ingress for every inbound request, with the protocol diagnostics method, protocol_version, result_kind and error_category and the request size against the configured limit, and a2a_egress for every dispatch, with the retained-state pressure counters (egress_entries, egress_reserved_cached_bytes and the cumulative reclaimed, saturation, oversize, conflict, in-progress and cached-hit totals). A2A events use result values accepted, rejected, failed or, for a caller that gave up mid-request, canceled. The field list is in audit your workflows.

Errors before authority is verified use the sidecar's error envelope with a stable code. The A2A-specific codes are:

Code Where Meaning
ERR_A2A_OVERLOADED inbound The concurrency limit is reached; the peer backs off
ERR_MISSING_AAC_MACAROON, ERR_MISSING_DPOP_PROOF inbound The caller is not an AAC sidecar, or sent the credential headers wrongly
ERR_REQUEST_TOO_LARGE; ERR_INVALID_REQUEST with HTTP 415 inbound and dispatch The body exceeds max_request_body_bytes; the content type is not JSON or the body is compressed
ERR_A2A_AUTHORITY_REGISTRATION_FAILED inbound The verified authority could not be retained for continuation
ERR_INVALID_AGENT_RESPONSE, ERR_AGENT_UNREACHABLE, ERR_AGENT_TIMEOUT inbound Your handler answered outside the profile, was not reachable, or exceeded the deadline
ERR_A2A_DISPATCH_FAILED, ERR_A2A_DISPATCH_TIMEOUT, ERR_A2A_DISPATCH_UNCERTAIN, ERR_A2A_REMOTE_REJECTED dispatch Delivery to the peer failed, timed out, has an unknown outcome, or was refused by the peer
ERR_EGRESS_DISPATCH_IN_PROGRESS, ERR_EGRESS_IDEMPOTENCY_CONFLICT, ERR_EGRESS_IDEMPOTENCY_SATURATED, ERR_EGRESS_RESPONSE_TOO_LARGE dispatch Retained-state outcomes described in the envelope responses above
ERR_CONTINUATION_AUTHORITY_UNAVAILABLE dispatch No live retained authority for a continue dispatch

After authority is verified, A2A protocol errors are JSON-RPC errors under HTTP 200, with the request id preserved: -32700 parse error, -32600 invalid request, -32601 unknown method, -32602 invalid parameters, -32004 unsupported operation and -32009 unsupported version. Inspect the JSON body even when the transport succeeded. The full error reference covers every code with its next action, including the authority and recipient refusals that A2A shares with native delivery.