A2A Gateway API
Use the A2A Gateway API when one agent needs to delegate work to a registered peer under workspace policy, approval, loop, identity, and audit controls.
Not included in DVARA Open Source.
Choose an operation
| Endpoint | Purpose |
|---|---|
GET /a2a/{agentId}/.well-known/agent-card.json | Read the governed Agent Card |
GET /a2a/.well-known/ard.json | List the agents your key may use, as an ARD manifest (experimental, off by default; see discovery endpoints) |
POST /a2a/{agentId}/message:send | Send a message and wait for the result |
POST /a2a/{agentId}/message:stream | Send a message and stream task updates |
POST /a2a/{agentId}/tasks:get | Read one task |
POST /a2a/{agentId}/tasks:list | List tasks |
POST /a2a/{agentId}/tasks:cancel | Cancel a task |
POST /a2a/{agentId}/tasks:resubscribe | Resume task event streaming |
POST /a2a/{agentId}/pushConfig:set | Create or replace push configuration |
POST /a2a/{agentId}/pushConfig:get | Read push configuration |
POST /a2a/{agentId}/pushConfig:list | List push configurations |
POST /a2a/{agentId}/pushConfig:delete | Remove push configuration |
POST /a2a/{agentId}/push:notify/{callback} | Receive a governed push notification from the peer |
POST /a2a/{agentId} | Any of the operations above, named by the JSON-RPC method (from 1.9.0) |
Every request uses the target's registered agentId. DVARA resolves the
workspace from the bearer API key before it looks up the target.
Name the method
A2A 1.0 renamed every JSON-RPC method. From 1.9.0 DVARA knows both sets of names and uses each with its own version:
| A2A 0.3 | A2A 1.0 | Path |
|---|---|---|
message/send | SendMessage | message:send |
message/stream | SendStreamingMessage | message:stream |
tasks/get | GetTask | tasks:get |
tasks/list | ListTasks | tasks:list |
tasks/cancel | CancelTask | tasks:cancel |
tasks/resubscribe | SubscribeToTask | tasks:resubscribe |
tasks/pushNotificationConfig/set | CreateTaskPushNotificationConfig | pushConfig:set |
tasks/pushNotificationConfig/get | GetTaskPushNotificationConfig | pushConfig:get |
tasks/pushNotificationConfig/list | ListTaskPushNotificationConfigs | pushConfig:list |
tasks/pushNotificationConfig/delete | DeleteTaskPushNotificationConfig | pushConfig:delete |
You can call an operation in two ways. Post to its path, as in the first
table; the path names the operation. Or post a standard JSON-RPC request to
/a2a/{agentId}, the address in DVARA's Agent Card, and name the operation in
method, as stock A2A clients do:
curl --silent https://<dvara-host>/a2a/<agent-id> \
--header "Authorization: Bearer $DVARA_API_KEY" \
--header "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":"1","method":"tasks/get","params":{"id":"<task-id>"}}'
Both ways run the same governance and give the same answer.
DVARA reads method in the version you name (see below):
| You send | DVARA answers |
|---|---|
| An A2A 0.3 name, as a 0.3 request | The operation |
| An A2A 1.0 name, as a 0.3 request, or an unknown name | 404, JSON-RPC -32601 (method not found), naming the right method |
No method | 400, JSON-RPC -32600 |
| An A2A 1.0 name, as a 1.0 request | The operation, read and answered in the A2A 1.0 shape (see "Send A2A 1.0 messages" below) |
DVARA calls each peer agent with the method name of the version it uses for
that peer: GetTask for a peer it reaches in A2A 1.0, tasks/get for a peer it
reaches in 0.3. Audit events keep one name for each operation (the 0.3 name),
and also record the name your client sent and the name sent to the peer.
Name the A2A version
From 1.9.0, DVARA reads the A2A protocol version of every request, as the A2A
specification asks. Send it in the A2A-Version header, or as an
A2A-Version query parameter:
A2A-Version: 0.3
| You send | What happens |
|---|---|
| No version | The request is served as A2A 0.3, as the specification requires. Existing 0.3 clients need no change. |
0.3 (a patch such as 0.3.1 is ignored) | The request is served. |
1.0 | The request is served in the A2A 1.0 shape. |
| Any other version | Refused with 400 before the request is authenticated. |
A refused version gets the A2A VersionNotSupportedError, and the versions
DVARA serves:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32009,
"message": "A2A protocol version 0.5 is not supported. Supported versions: 0.3, 1.0",
"data": {
"type": "VersionNotSupportedError",
"requestedVersion": "0.5",
"supportedVersions": ["0.3", "1.0"],
"code": "a2a_version_not_supported",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}
}
}
This is the A2A 0.3 form, which a version starting 0. gets. From 1.9.0, any
other refused version, such as 2.0, gets the A2A 1.0 form, where data is a
list holding one google.rpc.ErrorInfo (see Read a refusal):
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32009,
"message": "A2A protocol version 2.0 is not supported. Supported versions: 0.3, 1.0",
"data": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "VERSION_NOT_SUPPORTED",
"domain": "a2a-protocol.org",
"metadata": {
"code": "a2a_version_not_supported",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"requestedVersion": "2.0",
"supportedVersions": "0.3,1.0"
}
}]
}
}
An operator who wants to hold DVARA at A2A 0.3 can set
a2a.protocol.serve-v1: false. DVARA then refuses a 1.0 request, lists only
0.3 as supported, and calls every peer in 0.3.
DVARA also names the version on every call it makes to a peer agent, chosen
from that peer's Agent Card (see below). If the peer answers that it does not
support that version, your call fails with 502 a2a_peer_version_not_supported,
and the message names the peer. Push notifications from peers are not checked
for a version. The Agent Card follows its own rule, from 1.9.0: see
Read the card in your version.
How DVARA talks to each peer
From 1.9.0, DVARA reads each peer's Agent Card to decide how to call it, as the
A2A specification asks a client to. It goes through the card's
supportedInterfaces list in the order the peer wrote it, and uses the first
entry that it can speak: the JSON-RPC binding at a version DVARA serves, or,
from 1.9.0, the HTTP+JSON (REST) binding at A2A 1.0. It sends that entry's
tenant, if it has one.
| The peer's card lists | DVARA calls it with |
|---|---|
No supportedInterfaces (an A2A 0.3 card) | A2A 0.3 over JSON-RPC |
| A JSON-RPC 1.0 entry, then a JSON-RPC 0.3 entry | A2A 1.0, the first entry |
| A JSON-RPC 0.3 entry, then a JSON-RPC 1.0 entry | A2A 0.3, the first entry |
| An HTTP+JSON 1.0 entry, then a JSON-RPC 1.0 entry | A2A 1.0 over HTTP+JSON, the first entry (from 1.9.0) |
| A JSON-RPC 1.0 entry, then an HTTP+JSON 1.0 entry | A2A 1.0 over JSON-RPC, the first entry |
| Only gRPC entries, or only HTTP+JSON 0.3 entries | Nothing: your call fails with 502 a2a_peer_protocol_unsupported |
When no entry fits, the message lists what the peer offers and why each entry was passed over, and nothing is sent to the peer. Your client and the peer do not need the same version: DVARA translates between them (see below).
Which address DVARA calls
Each entry on the card has its own url. DVARA prefers an entry at the
endpoint the agent was registered with. From 1.9.0, when the chosen entry
lists another address, DVARA calls that address, as the A2A specification asks,
but only after it passes the same checks as the registered endpoint, on every
call:
- It is on the same scheme, host and port as the registered endpoint. Only the
path may differ, so a peer can serve A2A 1.0 at
/a2a/v1beside/a2a. - It is a plain
https://address (orhttp://where your deployment allows plain HTTP for development), with no user name, password or fragment. - The address DVARA connects to passes the same network checks as the registered endpoint.
- The card's signature allows it. A card whose signature is invalid never gives an address. If you pinned a public key for the agent, or require signed cards, only an address that the verified signature covers is used.
If the address fails a check, the call fails with
502 a2a_peer_url_refused and a message that names the check. Nothing is sent,
and DVARA does not quietly call the registered endpoint instead. The refusal is
recorded with the hop.
If a peer's card lists only a public name while you registered an internal one, calls to it now fail this way. Register the address the card lists, or ask the peer to list your registered endpoint on its card.
Bindings DVARA supports
DVARA calls peers over two of the A2A bindings:
- JSON-RPC, at A2A 0.3 or 1.0.
- HTTP+JSON (REST), at A2A 1.0, from 1.9.0.
DVARA does not support the gRPC binding, for calls to peers or for calls to
DVARA. The A2A specification makes every binding optional, and the official
A2A SDKs and the hosted agent platforms we checked all serve JSON-RPC or REST.
If your agent serves gRPC, also offer JSON-RPC or HTTP+JSON on its Agent Card.
A gRPC entry is passed over as gRPC is not supported.
From 1.9.0, Flightdeck checks this when an agent is registered or edited, so
you find out then rather than at the first call. If the peer's Agent Card offers
no binding DVARA supports, the save is refused with a message that lists what
the card offers. An operator can tick Save anyway for a special case, such
as a peer that is about to add a JSON-RPC entry. That choice is recorded in the
audit log, and calls to the agent still fail with
502 a2a_peer_protocol_unsupported until its card offers a supported binding.
If Flightdeck cannot read the card when you save, the save goes ahead.
Peers that speak HTTP+JSON
From 1.9.0, DVARA can call a peer that offers the A2A HTTP+JSON (REST) binding.
Nothing changes for your client: you still call DVARA as before, and DVARA
calls the peer at the REST address for each operation under the registered
endpoint, such as POST <endpoint>/message:send or GET <endpoint>/tasks/{id}.
The same checks run as for a JSON-RPC peer: policy, PII, limits, approval,
the agent's credential, the extensions you are allowed, and the audit record.
Streams work the same way.
DVARA reads the peer's answer strictly, as it does for any A2A 1.0 peer. An
answer it cannot read fails your call with 502 a2a_upstream_invalid_response,
and nothing from it reaches you. When the peer answers with an A2A error, you
get that error in your own version: a task the peer does not know is JSON-RPC
-32001, as it would be from a JSON-RPC peer. The peer's own error text is not
passed on.
DVARA itself is still called over JSON-RPC only, and the Agent Card it serves lists only JSON-RPC. It keeps its choice for as long as it keeps the card, and makes it again when the card changes. Each call's record shows the version and binding used.
Send A2A 1.0 messages
From 1.9.0 DVARA serves A2A 1.0 as well as 0.3. A2A 1.0 changed how messages, tasks and stream events are written, not only how methods are named:
| A2A 0.3 | A2A 1.0 | |
|---|---|---|
| A part of a message | {"kind": "text", "text": "…"}, {"kind": "file", "file": {…}}, {"kind": "data", "data": {…}} | {"text": "…"}, {"raw": "<base64>"}, {"url": "…"}, {"data": …}, each with an optional mediaType and filename |
| Roles and task states | user, agent; working, completed, input-required … | ROLE_USER, ROLE_AGENT; TASK_STATE_WORKING, TASK_STATE_COMPLETED … |
| The answer to a message | the task or the message | {"task": …} or {"message": …} |
| A stream event | the object, with kind, and final on a status update | {"statusUpdate": …}, {"artifactUpdate": …}, {"task": …} or {"message": …}; no final |
| A push notification config | taskId and a nested pushNotificationConfig | flat: taskId, id, url, token, authentication |
| Listing tasks | not a JSON-RPC method | ListTasks, with pageSize (1 to 100) and pageToken; every answer has nextPageToken, empty on the last page |
A 1.0 request looks like this:
curl --silent https://<dvara-host>/a2a/<agent-id> \
--header "Authorization: Bearer $DVARA_API_KEY" \
--header "A2A-Version: 1.0" \
--header "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":"1","method":"SendMessage","params":{"message":
{"messageId":"m-1","role":"ROLE_USER","parts":[{"text":"Plan a trip to Lisbon"},
{"data":{"days":3}}]}}}'
Your client and the peer can speak different versions. DVARA translates
every message, answer, stream event and push notification between the version
your client sends and the version it uses for the peer. A 1.0 client can call a
0.3 agent, and a 0.3 client can call a 1.0 agent. A 0.3 client still gets final
on stream events, worked out from the task's state. A stream ends when it
closes, with no extra [DONE] line. From 1.9.0 that is true for a 0.3 client
too: every A2A stream event is a JSON-RPC answer, and the official 0.3 SDK
failed on the extra line after a complete answer.
Push config lists are paged for a 0.3 agent. An A2A 0.3 agent lists every
push config in one answer. From 1.9.0, when a 1.0 client sends pageSize or
pageToken to such an agent, DVARA asks the agent for the whole list and returns
the page, with a nextPageToken for the rest. Use the token DVARA gave you; a
token it did not give is refused with -32602. Earlier releases refused the
paged request.
Governance covers every kind of part. DVARA reads each message into one form before any check runs, so PII redaction and blocking, policy and the task checks apply the same way whichever version you send. PII is checked in text parts and in every string inside a data part. File content is not read. From 1.9.0 a stream is checked the same way: data parts in the message you stream, and in every event the agent streams back, are redacted or blocked like text. Each string in a data part is checked as a value of its own.
What DVARA does not understand, it refuses. It never skips a part it cannot read, because a part it skipped is a part nobody checked:
| What happens | Answer |
|---|---|
Your request has a part, member or value its version does not define, such as {"image": …} or "role": "user" in a 1.0 request | 400, JSON-RPC -32602, with the place and the reason, for example params.message.parts[1]: unknown member image. Nothing is sent to the peer |
Your request has something the peer's version cannot carry, such as a filename on a text part for a 0.3 peer, or ListTasks to a 0.3 peer (A2A 0.3 has no JSON-RPC method for it) | 400, JSON-RPC -32004, a2a_untranslatable. DVARA refuses rather than drop part of your message |
| The peer answers with a shape DVARA cannot read | 502, JSON-RPC -32006, a2a_upstream_invalid_response. On a stream, one error event, and the stream ends |
| The peer's answer cannot be carried in your version | 502, a2a_upstream_untranslatable |
DVARA has been checked against the official A2A Python SDK (version 1.0.3), as the client and as the agent, and from 1.9.0 against its 0.3 release (0.3.26) as a streaming client. These checks run on every change to the A2A gateway.
Choose the content type
From 1.9.0, DVARA accepts two content types for a request body on every A2A operation:
application/json, which the A2A JSON-RPC binding uses.application/a2a+json, the A2A media type. The A2A specification names it for its HTTP+JSON binding, so a client built for that binding can send it unchanged.
Both are read the same way, under the same limits and governance. Any other
type, such as text/plain or YAML, is refused with 415 a2a_unsupported_media_type, and the Accept header of the answer names the
two types DVARA reads.
DVARA answers with application/json, as the JSON-RPC binding requires. If
your client accepts only application/a2a+json, DVARA answers with that type
instead; the body is the same. Streams are text/event-stream.
DVARA sends a JSON-RPC peer application/json and, from 1.9.0, an HTTP+JSON
peer application/a2a+json, and it reads an answer in either JSON type. If a peer answers with anything
else, such as an HTML error page, your call fails with 502 a2a_upstream_content_type, and the message names the type the peer sent.
Authenticate and scope the call
Workspace-facing A2A operations require a DVARA workspace API key:
Authorization: Bearer <your-api-key>
| Scope | Operations |
|---|---|
a2a:read | tasks:get, tasks:list, tasks:resubscribe, and push-configuration reads |
a2a:send | message:send, message:stream, tasks:cancel, and push-configuration writes or deletion |
An unscoped API key is unrestricted. Agent Card discovery still requires a valid workspace key, but it is not narrowed by an A2A scope in 1.8.
Send a governed message
Replace the host, target agent ID, and workspace API key:
curl --silent https://<dvara-host>/a2a/<agent-id>/message:send \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": "hop-1042",
"method": "message/send",
"params": {
"metadata": {"skill": "summarize"},
"message": {
"role": "user",
"parts": [
{"kind": "text", "text": "Summarize claim CLM-1042 for review."}
]
}
}
}'
A completed peer call returns an A2A message or task inside the JSON-RPC envelope:
{
"jsonrpc": "2.0",
"id": "hop-1042",
"result": {
"task": {
"id": "task-7f12"
}
}
}
If policy denies the hop, DVARA refuses it with a2a_policy_denied and does not
call the peer (HTTP 200 with a JSON-RPC error from 1.9.0; see Read a
refusal).
Open Agents → A2A Hops to inspect the source, target, decision, status, and latency. The same hop writes intent and result or denial evidence to Governance → Audit, joined by its trace information.
Stream a message or resume a task
message:stream and tasks:resubscribe return Server-Sent Events. They use the
same authentication and governance checks as their unary forms. Streaming is
on by default; when a2a.streaming.enabled=false, either route is refused with
a2a_streaming_disabled.
From 1.9.0, a governance refusal, or an agent that answers with an error
instead of a stream, is a plain JSON-RPC error response, not an event stream.
Before, it was delivered as one SSE error event. If the peer fails or stalls
after the stream opens, DVARA sends an error event
and does not send [DONE]. Treat that event as the final outcome rather than a
partial success. A stream that completes ends when it closes. From 1.9.0 DVARA
sends no [DONE] line at either A2A version.
Discover the governed Agent Card
GET /a2a/{agentId}/.well-known/agent-card.json returns the card JSON directly,
not a JSON-RPC envelope. DVARA resolves the agent inside the caller's workspace,
filters its skills with that workspace's allow-list, and serves the cached card
until its discovery TTL expires.
From 1.9.0 the card describes DVARA, not the peer, because your client talks
to DVARA. Its supportedInterfaces lists DVARA's address for the agent
(https://<gateway>/a2a/{agentId}) with the JSON-RPC binding, once for each A2A
version DVARA serves, 1.0 and then 0.3. For A2A 0.3 clients, url,
preferredTransport (JSONRPC), additionalInterfaces and protocolVersion
(0.3) say the same. The peer's own address and versions are not shown. Post
JSON-RPC requests to that address, or send each operation to that address plus
its path, such as /message:send (see Name the method).
Add ?refresh=true to fetch the peer again immediately. If the agent has a
pinned public key, DVARA verifies the card signature and reports the trust
state in x-dvara-signature-trust. From 1.9.0 the signature is checked the way
the card's own A2A version defines, and the served card does not include the
peer's signatures, because they cover the peer's card, not the one DVARA
serves. From 1.9.0, an agent with a pinned key gets
502 a2a_card_signature_invalid for any card that is not VERIFIED, including
an unsigned one, and each signature must carry kid and typ JOSE. A fetch failure, or a card whose structure does not match its version,
uses a valid cached card when one is available; otherwise it returns
502 a2a_card_unavailable.
From 1.9.0 the answer carries Cache-Control: private, max-age=60,
Vary: Authorization, X-Dvara-Delegated-Authorization, A2A-Version and an
ETag. Send the ETag in If-None-Match to get 304 Not Modified with no
body when the card has not changed:
curl --include https://<gateway>/a2a/researcher/.well-known/agent-card.json \
--header 'Authorization: Bearer <your-api-key>' \
--header 'If-None-Match: "<etag from the last answer>"'
DVARA checks the key and governs the card before it compares the tag, so a
refused request gets its error, not a 304. Callers who see different cards,
for example two workspaces with different allow lists, get different tags.
DVARA keeps the peer's own card for as long as the peer's caching headers say,
between 30 seconds and one hour, and then asks the peer with its ETag.
Read the card in your version
From 1.9.0 DVARA serves the Agent Card in the A2A version your client names in
the A2A-Version header. A2A 0.3 and 1.0 write a card's security schemes in
different shapes under the same name, so one card cannot serve both: an SDK
reading the other version's shape refuses the card or misreads it.
| You send | The card you get |
|---|---|
No A2A-Version header | The A2A 1.0 card. |
A2A-Version: 1.0 | The A2A 1.0 card: supportedInterfaces, security schemes such as {"httpAuthSecurityScheme": {"scheme": "bearer"}}, and securityRequirements. It also carries url, preferredTransport and protocolVersion, so a 0.3 client that sends no header still finds DVARA's address. |
A2A-Version: 0.3 | The A2A 0.3 card: url, preferredTransport, additionalInterfaces and protocolVersion 0.3, security schemes such as {"type": "http", "scheme": "bearer"}, and security. No supportedInterfaces. |
| Any other version | Refused with 400 and the A2A VersionNotSupportedError, as for any A2A request. |
Only the header picks the card's version; an A2A-Version query parameter does
not. The extended Agent Card follows the same rule.
In both versions the card's extended-card flag (capabilities.extendedAgentCard
at 1.0, supportsAuthenticatedExtendedCard at 0.3) is false, because DVARA
does not serve the extended-card method. The extended card, when DVARA can
fetch it for you, is the card at this same address.
An A2A 0.3 client should send A2A-Version: 0.3 when it fetches the card.
Without it, the client gets the 1.0 card and cannot read its security schemes.
DVARA translates each security scheme the peer declares, whichever version the
peer wrote its card in. A few things have no A2A 0.3 form: an OAuth 2.0 device
code flow, and the pkceRequired flag. DVARA leaves them out of the 0.3 card
and lists each, with the reason, in x-dvara-security-schemes-omitted. A
security requirement that names a scheme left out is left out too.
With a2a.protocol.serve-v1: false, DVARA serves 0.3 only, so the card is the
0.3 card and a request for the 1.0 card is refused.
Check DVARA's signature on the card
From 1.9.0 the card DVARA serves carries DVARA's own signatures, once an
owner has made a card signing key in Flightdeck. Each signature's protected
header has alg (ES256), kid, typ (JOSE) and jku, which points to the
public keys:
curl https://<gateway>/a2a/.well-known/jwks.json
No API key is needed for the key set. Check the card with your A2A SDK's
signature verifier, taking the key by its kid from that key set. The card is
signed in the version you asked for, so a 0.3 SDK checks the 0.3 card and a 1.0
SDK the 1.0 card. A rotated-out key stays in the key set for 30 days. The same
card keeps the same signature, so its ETag does not change.
Ask for an A2A extension
From 1.9.0 DVARA governs A2A extensions. An extension is named by a URI and
can add data, methods and task states to A2A. Ask for one in the
A2A-Extensions header, as the A2A specification describes:
curl --include https://<gateway>/a2a/researcher \
--header 'Authorization: Bearer <your-api-key>' \
--header 'Content-Type: application/json' \
--header 'A2A-Extensions: https://a2a-protocol.org/extensions/timestamp/v1' \
--data '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{"kind":"message","messageId":"m-1","role":"user","parts":[{"kind":"text","text":"Summarise the report"}]}}}'
DVARA passes an extension to the agent only when all three are true:
- you asked for it in
A2A-Extensions; - your workspace allows it (
a2a.extensions.allowed; nothing is allowed until an operator allows it); - the agent's Agent Card declares it.
The answer's A2A-Extensions header lists the extensions the agent says it
activated. A stream does not carry this header. Extension URIs that are not in
use are removed from the extensions list of messages and artifacts in both
directions, and the Agent Card DVARA serves lists only the extensions your
workspace allows.
If the agent's card marks an extension as required:
| Your workspace | You asked for it | Result |
|---|---|---|
| Does not allow it | Either | 403, JSON-RPC -32000, code a2a_extension_not_allowed. The agent is not called |
| Allows it | No | 400, JSON-RPC -32008 (ExtensionSupportRequiredError), code a2a_extension_support_required. Send the header and try again |
| Allows it | Yes | The call goes ahead |
DVARA does not forward methods an extension adds. It cannot read them, so it
could not check them for PII, policy or limits. Such a call gets 404, JSON-RPC
-32601.
Govern push notifications
The pushConfig:* routes manage the peer's push-notification configuration.
pushConfig:set validates the workspace webhook address and refuses loopback,
private, cloud-metadata, and plain HTTP targets before contacting the peer.
When a2a.push.callback-base-url is configured, DVARA replaces the workspace
webhook details sent to the peer with a sealed Gateway callback. The peer posts
updates to:
POST /a2a/{agentId}/push:notify/{callback}
X-A2A-Notification-Token: <notification-token>
The peer callback takes no DVARA API key. The sealed path and notification token identify the expected agent and task; an agent with a pinned key must also send a signed bearer JWT. DVARA checks and PII-scans the notification, records it, and then sends it to the workspace webhook. Duplicate notifications are accepted without a second delivery.
If the Gateway has no public callback base URL, governed push configuration is
refused with 503 a2a_push_not_available, unless the workspace explicitly uses
direct delivery. Direct delivery sends the workspace webhook to the peer and
does not put the Gateway on the notification path.
Apply the request and response limits
| Boundary | Default | Failure |
|---|---|---|
Text across an outbound message:send or message:stream message | 50,000 characters | 413 a2a_message_too_large |
| Whole peer answer, task response, or Agent Card | 16 MiB | 502 a2a_response_too_large; Agent Card discovery uses a2a_card_unavailable when no cached card can be served |
message:stream or tasks:resubscribe stream | 64 MiB | The stream ends with an SSE error event |
| Incoming governed push-notification body | 1 MiB | 413 a2a_push_too_large |
The message limit runs before policy and PII scanning. Response limits cover the bytes DVARA reads from a peer. Operators can change the fleet defaults; the response limits can also be set per workspace.
Read a refusal
From 1.9.0, every refusal on a JSON-RPC call is a JSON-RPC error that an A2A
client can read, in the form of the A2A version you sent. Before, a refusal by
DVARA itself, such as a policy denial, put DVARA's code as text in
error.code, where the specification requires a number.
error.codeis always a number. Where the A2A specification or JSON-RPC names the error, it is theirs:-32009version not supported,-32602invalid parameters,-32004unsupported operation,-32006an answer from the peer that does not follow the specification,-32700a body that is not JSON. A decision DVARA made (policy, PII, limits, approval, loop, session kill, key, delegation, an unreachable peer) is-32000.- DVARA's own code is in
error.data. With A2A 0.3 (or no version),datais an object:data.codeis the code, such asa2a_policy_denied, withdata.typeanddata.trace_id. With A2A 1.0,datais a list holding onegoogle.rpc.ErrorInfo, as the 1.0 specification asks: the code is inmetadata.code, and the trace ID inmetadata.traceId. - The domain says who refused. In the 1.0 form, a refusal by DVARA has
domaindvarahq.comand its code in capitals as thereason(A2A_POLICY_DENIED). An error the A2A specification defines hasdomaina2a-protocol.organd the specification'sreason(TASK_NOT_FOUND). - A peer's A2A error reaches you as the peer sent it. If the agent answers,
for example,
-32001task not found, you get-32001and its reason, and DVARA's codea2a_upstream_error. The rest of the peer's error detail is not passed on.
A policy denial at A2A 1.0:
{
"jsonrpc": "2.0",
"id": "1",
"error": {
"code": -32000,
"message": "Hop denied by A2A policy",
"data": [{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "A2A_POLICY_DENIED",
"domain": "dvarahq.com",
"metadata": { "code": "a2a_policy_denied", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736" }
}]
}
}
The same refusal at A2A 0.3 has "data": {"type": "a2a_error", "code": "a2a_policy_denied", "trace_id": "…"}.
On a stream, DVARA's own error event (a peer that stalls, a stream cut at the
size limit or stopped for PII or a session kill) is a JSON-RPC error with your
call's id, in the same form. From 1.9.0, a stream refused before it opens is
not a stream: if DVARA refuses it, or the agent answers with an error instead of
a stream (for example TaskNotFoundError when you subscribe to a task it does
not know), you get a plain JSON-RPC error response (application/json), as for
a call that is not a stream. A refused Agent Card request
answers an A2A 1.0 client with a google.rpc.Status body: {"error": {"code": 404, "status": "NOT_FOUND", "message": "…", "details": [ErrorInfo]}}; without a
version it keeps {"error": {"code": "a2a_agent_not_found", …}}.
The HTTP status of a refusal
From 1.9.0, DVARA answers a refusal of a JSON-RPC call with HTTP 200, as the
official A2A SDK's own server does. The JSON-RPC binding of A2A gives an error no
HTTP status; the official A2A Python SDK reads the error only from a 200
answer, and raises a plain HTTP error on any other status. So an SDK client now
gets the A2A error DVARA sent: for example JSON-RPC Error -32000 for a policy
denial, or TaskNotFoundError when the agent does not know the task. Branch on
error.code and DVARA's code in error.data, not on the status.
One refusal keeps its status: a missing, expired or revoked key is still
answered 401, with the same JSON-RPC body. A2A checks credentials at the HTTP
level, and the A2A 0.3 specification asks for 401 there.
Also from 1.9.0, when the agent answers that your request is invalid
(-32600 to -32603, for example -32602 invalid parameters), you get the
agent's code. Before, it reached you as -32000.
Before 1.9.0 the status was the refusal's own, such as 403 for a policy
denial and 502 for an agent's error. The operator setting
a2a.protocol.json-rpc-error-status: refusal brings those statuses back. These
keep their status either way: an Agent Card request, a request that is not a
POST, a body that is not application/json (415), and a stream (which was
always 200, with the error as its event).
DVARA's own records still tell refusals apart: its request log and the
a2a_requests_total metric carry the refusal's code and JSON-RPC code, and the
hop history shows the refusal's status, such as 403.
Diagnose a failed call
A missing, expired, or revoked workspace key returns 401. A key without the
required A2A scope is refused with a2a_scope_denied (HTTP 200 from 1.9.0;
see The HTTP status of a refusal). A target outside the caller's workspace is
not exposed. A message or task refusal is a JSON-RPC error with DVARA's code and
the request trace ID in error.data (see Read a refusal).
A stream refused before it opens is a plain JSON-RPC error; a failure after it
opens is one JSON-RPC error event; Agent Card discovery returns an
ordinary HTTP JSON error.
Peer discovery, signature, credential, policy, approval, loop, size, and transport failures use distinct error codes so you can separate configuration failures from governed denials. See Error handling for the status and code table.
For delegation chains, approval behavior, streaming, push callbacks, and Agent Card trust, continue with Govern agent-to-agent calls with the A2A Gateway.