Skip to Content
DeploymentsA2A integration

A2A integration

By the end of this page you will be able to call a deployed qRaptor AI Agent from any A2A-compliant client — an agent framework such as Google ADK or LangGraph, an enterprise platform with A2A support, or your own code — and handle streaming, long-running work and errors correctly.

What A2A is

A2A (Agent2Agent) is an open protocol for one agent to call another over HTTP. A calling agent fetches a public agent card describing what the agent can do, then sends it work as JSON-RPC 2.0 requests.

qRaptor implements A2A inbound: a deployed AI Agent becomes a standards-compliant A2A server that any client can call.

Versions qRaptor supports

A2A versionStatus in qRaptorCard location
0.3.0Supported — default/.well-known/agent-card.json
0.2.x (0.2.1–0.2.6)Supported/.well-known/agent.json
1.0.xIn validation — released separately/.well-known/agent-card.json
0.1.0Not supported (uses the removed tasks/send method)

A2A 1.0 is currently in validation and will be enabled in a separate release. Until then, agents serve 0.3.0 by default and 0.2.x for clients on that generation — which together cover the message/send clients in use today. If your roadmap depends on 1.0, talk to us and we will confirm timing.

Both card locations are always served. Each card advertises a url pinned to its own version, so a client that POSTs to whatever url it read automatically gets the matching behaviour. You can also pin explicitly:

EndpointVersion served
POST /agents/{id}/a2aThe agent’s configured default (0.3.0 unless changed)
POST /agents/{id}/a2a/v0.3A2A 0.3.0
POST /agents/{id}/a2a/v0.2A2A 0.2.6

Prerequisites

  • Admin role.
  • A deployment containing an AI Agent (A2A is not available for Task Agents).

Enable A2A on the agent

Open the deployment’s Agents tab, expand the agent’s Settings, and turn on A2A Protocol. The panel then shows the protocol version plus copy buttons for the endpoint and card URLs.

Create an a2a API key

Go to API Keys → Create key and choose the a2a scope. The key is shown once — store it securely.

The a2a scope covers the whole JSON-RPC endpoint. It cannot be scoped per method, because the method name travels in the request body.

Give the client the card URL

An A2A client normally needs only the card URL:

https://your-deployment.qraptor.app/agents/{agentId}/.well-known/agent-card.json

It reads the url from the card and sends requests there with your API key.

Discovery

The agent card is public — no API key. That is required by the specification so clients can discover capabilities before authenticating.

curl https://your-deployment.qraptor.app/agents/{agentId}/.well-known/agent-card.json
{ "protocolVersion": "0.3.0", "name": "Revenue Analyst", "description": "Answers questions about sales and revenue", "url": "https://your-deployment.qraptor.app/agents/{agentId}/a2a/v0.3", "preferredTransport": "JSONRPC", "version": "3", "capabilities": { "streaming": true, "pushNotifications": false, "stateTransitionHistory": true }, "defaultInputModes": ["text/plain", "application/json"], "defaultOutputModes": ["text/plain", "text/markdown", "application/json"], "securitySchemes": { "apiKey": { "type": "http", "scheme": "bearer" } }, "security": [{ "apiKey": [] }], "skills": [ { "id": "revenue-lookup", "name": "Revenue lookup", "description": "Look up revenue for a date or period", "tags": ["sales"], "examples": ["What was total revenue on 2026-07-20?"] } ], "provider": { "organization": "qRaptor", "url": "https://qraptor.ai" } }

capabilities is truthful — anything advertised there can actually be used. pushNotifications reads false unless an operator enables it for the agent, because it makes qRaptor call an address you supply; see Push notifications.

Sending a message

message/send is the core method. It blocks until the agent answers, then returns a Task.

curl -X POST https://your-deployment.qraptor.app/agents/{agentId}/a2a \ -H "Authorization: Bearer qr_your_a2a_key" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": "req-001", "method": "message/send", "params": { "message": { "role": "user", "parts": [{ "kind": "text", "text": "What was total revenue on 2026-07-20?" }], "messageId": "msg-0001" } } }'
{ "jsonrpc": "2.0", "id": "req-001", "result": { "id": "b3f1e9c4-…", "contextId": "7a2d5e10-…", "kind": "task", "status": { "state": "completed", "timestamp": "2026-08-01T10:15:31Z" }, "artifacts": [ { "artifactId": "a1c9e7b2-…", "name": "agent_response", "parts": [{ "kind": "text", "text": "Total revenue on 2026-07-20 was £48,210." }] } ], "history": [] } }

Reading the result:

FieldMeaning
result.status.statecompleted, working, input-required, failed, canceled
result.artifacts[]The agent’s output. The text answer is the artifact named agent_response.
result.idTask id — use it with tasks/get or to continue the task.
result.contextIdConversation id — reuse it to keep memory across calls.
result.historyMessage history. Empty unless you ask for it (see below).

messageId must be unique per request. Sending the same messageId twice returns the stored task without re-running the agent and without charging credits again — retry safety, but it means a hard-coded id looks like nothing is happening.

Multi-turn conversations

Pass the same contextId on later sends and the agent keeps its conversation memory, exactly as a chat user would:

{ "params": { "message": { "role": "user", "parts": [{ "kind": "text", "text": "And the day before?" }], "messageId": "msg-0002", "contextId": "7a2d5e10-…" } } }

Sending files

A message may carry file parts alongside text. Two forms are accepted:

{ "params": { "message": { "role": "user", "parts": [ { "kind": "text", "text": "Summarize this invoice" }, { "kind": "file", "file": { "name": "invoice.pdf", "mimeType": "application/pdf", "bytes": "<base64-encoded content>" } } ], "messageId": "msg-0004" } } }
  • bytes — the file is stored against your subscription and handed to the agent as an attachment, exactly like a file uploaded in the chat UI. Max 20 MB decoded, 10 files per message.
  • uri — pass a link instead. qRaptor does not download it; the URI is handed to the agent’s tools, which fetch it if they need to. It must be https and resolve to a public address.

mimeType is required, and must be an accepted type — images, audio, video, PDF, text, JSON and Office documents. Anything else returns -32005. Check the agent card’s defaultInputModes to see what a given agent can actually process: an agent without a vision tool will accept a PNG but has no way to read it.

You can send a file with no text at all — the agent is prompted to review the attached files.

Asking for history

History is omitted by default. Request it explicitly:

{ "params": { "message": { "…": "…" }, "configuration": { "historyLength": 10 } } }

Long-running work

An agent that uses tools can run longer than an HTTP request should be held open. qRaptor waits up to the agent’s blocking budget (120 seconds by default), then detaches the run and answers with the task in state working:

{ "jsonrpc": "2.0", "id": "req-002", "result": { "id": "b3f1e9c4-…", "status": { "state": "working" }, "artifacts": [] } }

The agent keeps running. Poll with tasks/get using the task id until the state is terminal:

{ "jsonrpc": "2.0", "id": "req-003", "method": "tasks/get", "params": { "id": "b3f1e9c4-…", "historyLength": 0 } }

You can also opt out of blocking entirely and poll from the start:

{ "params": { "message": { "…": "…" }, "configuration": { "blocking": false } } }
💡

Any client that calls message/send should be prepared for state working. Treating it as a failure is the most common A2A integration bug — the work is running fine, the answer just is not ready yet.

Streaming

message/stream takes the same params and returns Server-Sent Events. Each frame is a JSON-RPC envelope whose result is an event:

data: {"jsonrpc":"2.0","id":"req-004","result":{"taskId":"…","contextId":"…","kind":"status-update","status":{"state":"working"},"final":false}} data: {"jsonrpc":"2.0","id":"req-004","result":{"taskId":"…","kind":"artifact-update","artifact":{"artifactId":"…","name":"agent_response","parts":[{"kind":"text","text":"Total revenue "}]},"append":true}} data: {"jsonrpc":"2.0","id":"req-004","result":{"taskId":"…","kind":"status-update","status":{"state":"completed"},"final":true}}
  • artifact-update frames with append: true are text deltas — concatenate them by artifactId.
  • The stream always ends with a frame carrying final: true.
  • If the connection drops, the task row is still authoritative: fall back to tasks/get.

Reconnecting to a stream

tasks/resubscribe reattaches to a stream you lost:

{ "jsonrpc": "2.0", "id": "req-007", "method": "tasks/resubscribe", "params": { "id": "b3f1e9c4-…" } }

It replays the events you missed, then tails live ones. Because events are held centrally rather than on one server, this works even when the reconnect lands on a different replica from the one running the agent. If the task already finished while you were away, you get a single final: true status event rather than an empty stream.

Push notifications

Instead of polling, you can register a webhook and have qRaptor push task updates to you. This is off by default — it makes qRaptor issue outbound requests to an address you control, so an operator must enable pushNotifications on the agent first. Until then these methods return -32003.

{ "jsonrpc": "2.0", "id": "req-008", "method": "tasks/pushNotificationConfig/set", "params": { "taskId": "b3f1e9c4-…", "pushNotificationConfig": { "url": "https://your-system.example.com/a2a/hook", "token": "your-shared-secret" } } }

token is required. Every delivery carries an X-A2A-Signature header — HMAC-SHA256(body, token), hex-encoded — so you can verify the request genuinely came from qRaptor. Verify it before trusting the payload.

Webhook rules:

  • HTTPS only, and the host must resolve to a public address. URLs pointing at private ranges, loopback or cloud metadata endpoints are rejected at registration with -32602, and re-checked before every delivery.
  • Redirects are not followed.
  • Delivery retries up to 3 times with backoff. A 4xx from your endpoint (other than 429) is treated as a permanent rejection and not retried.
  • The token is never echoed back by tasks/pushNotificationConfig/get.

Companion methods: tasks/pushNotificationConfig/get, /list and /delete.

Human-in-the-loop

If the agent hits an approval checkpoint, the task comes back as input-required with the prompt in status.message. Continue it by sending the reply with the same taskId:

{ "jsonrpc": "2.0", "id": "req-005", "method": "message/send", "params": { "message": { "role": "user", "parts": [{ "kind": "text", "text": "Approved" }], "messageId": "msg-0003", "taskId": "b3f1e9c4-…", "contextId": "7a2d5e10-…" } } }

Cancelling

{ "jsonrpc": "2.0", "id": "req-006", "method": "tasks/cancel", "params": { "id": "b3f1e9c4-…" } }

Only non-terminal tasks can be cancelled; anything already finished returns -32002.

Errors

A JSON-RPC transport returns HTTP 200 even for errors — always inspect the body, never just the status code.

CodeMeaningUsual cause
-32700Parse errorMalformed JSON
-32600Invalid requestMissing jsonrpc: "2.0" or method
-32601Method not foundMethod name typo
-32602Invalid paramsMissing message, empty parts, or no messageId
-32603Internal errorServer-side failure; data.code may say INSUFFICIENT_CREDITS
-32001Task not foundUnknown taskId (also returned for another tenant’s task)
-32002Task not cancelableTask already finished
-32003Push notifications unsupportedNot enabled for this agent
-32004Unsupported operationUnsupported protocol version, or A2A disabled on the agent
-32005Content type not supportedA file part whose mimeType is not accepted
-32007Extended card not configuredagent/getAuthenticatedExtendedCard; the public card already carries everything a client needs

An agent that fails is not a protocol error. If the agent ran and could not complete the task, you get a successful result whose status.state is failed, with the reason in status.message. A JSON-RPC error means the request never reached the agent. Handle both.

Gateway-level refusals do use HTTP status codes — 401/403 for a missing, expired or wrongly scoped key, 404 when A2A is not enabled on the agent, 429 when rate-limited — but the body is still JSON-RPC-shaped so your client can surface a code and message.

Rate limits, credits and idempotency

  • Rate limits are per agent, in requests per minute, set in the agent’s inline settings.
  • Credits are consumed exactly as for any other invocation. Insufficient credits produce -32603 with data.code = "INSUFFICIENT_CREDITS".
  • Idempotency is keyed on messageId per subscription: replaying one returns the stored task without re-running the agent or re-charging.

Client walkthroughs

Enterprise platforms

Many platforms (ITSM, workflow and agent-orchestration tools) can register an external A2A agent from its card. The steps are the same whichever you use:

  1. Enable A2A on the agent and create an a2a-scoped key.
  2. Register the agent in the calling platform using its card URL, https://your-deployment.qraptor.app/agents/{agentId}/.well-known/agent-card.json.
  3. Set the credential to Authorization: Bearer qr_….
  4. Invoke with message/send. Most platforms read the answer from result.artifacts[*].parts[*].text.
  5. If your agent can run long, confirm the platform tolerates a working result and polls tasks/get — or lower the agent’s blocking budget so it answers inside the platform’s request timeout.

Your own code

import requests, uuid BASE = "https://your-deployment.qraptor.app/agents/<agentId>" KEY = "qr_your_a2a_key" card = requests.get(f"{BASE}/.well-known/agent-card.json").json() res = requests.post( card["url"], headers={"Authorization": f"Bearer {KEY}"}, json={ "jsonrpc": "2.0", "id": "req-001", "method": "message/send", "params": { "message": { "role": "user", "parts": [{"kind": "text", "text": "What was total revenue on 2026-07-20?"}], "messageId": str(uuid.uuid4()), } }, }, ).json() if "error" in res: raise RuntimeError(res["error"]["message"]) task = res["result"] if task["status"]["state"] == "working": ... # poll tasks/get with task["id"] else: for artifact in task.get("artifacts", []): for part in artifact.get("parts", []): if part.get("kind") == "text": print(part["text"])

Migrating from the legacy task API

The pre-JSON-RPC REST endpoints still work but are deprecated:

LegacyReplacement
POST /agents/{id}/tasksmessage/send
GET /agents/{id}/tasks/{taskId}tasks/get
GET /agents/{id}/tasks/{taskId}/eventsmessage/stream
POST /agents/{id}/tasks/{taskId}/canceltasks/cancel

Two shape changes to watch:

  • Message parts are { "kind": "text", "text": "…" }, not { "type": "text", "content": "…" }. The old shape is still accepted so migrations do not break mid-flight, but new clients should send kind.
  • Fields are camelCase (contextId, artifactId, status.state), not snake_case (task_id, context_id).

Troubleshooting

SymptomCause
404 with "A2A is not enabled for this agent"Turn on A2A Protocol in the agent’s settings.
401 / 403Key missing, expired, revoked, or lacking the a2a scope.
-32602 on a request that looks rightCheck messageId is present and parts is non-empty.
The agent answers generically, as if it saw no inputParts are probably malformed — confirm kind/text (or legacy type/content) spelling.
Client hangs, then times outLong-running agent. Handle state working and poll tasks/get, or lower the blocking budget.
Card fetch returns 401Discovery is public — you are probably requesting a path that is not one of the two card URLs.
Empty historyExpected. Ask for it with configuration.historyLength or params.historyLength.
-32602 on a file partMissing mimeType, invalid base64, over 20 MB, more than 10 files, or a uri that is not public https.
Agent ignores an attached fileIt has no tool that reads that type. Check the card’s defaultInputModes and attach a vision / file-parse tool to the agent.