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 version | Status in qRaptor | Card location |
|---|---|---|
| 0.3.0 | Supported — default | /.well-known/agent-card.json |
| 0.2.x (0.2.1–0.2.6) | Supported | /.well-known/agent.json |
| 1.0.x | In validation — released separately | /.well-known/agent-card.json |
| 0.1.0 | Not 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:
| Endpoint | Version served |
|---|---|
POST /agents/{id}/a2a | The agent’s configured default (0.3.0 unless changed) |
POST /agents/{id}/a2a/v0.3 | A2A 0.3.0 |
POST /agents/{id}/a2a/v0.2 | A2A 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.jsonIt 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:
| Field | Meaning |
|---|---|
result.status.state | completed, working, input-required, failed, canceled |
result.artifacts[] | The agent’s output. The text answer is the artifact named agent_response. |
result.id | Task id — use it with tasks/get or to continue the task. |
result.contextId | Conversation id — reuse it to keep memory across calls. |
result.history | Message 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 behttpsand 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-updateframes withappend: trueare text deltas — concatenate them byartifactId.- 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
4xxfrom your endpoint (other than429) 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.
| Code | Meaning | Usual cause |
|---|---|---|
-32700 | Parse error | Malformed JSON |
-32600 | Invalid request | Missing jsonrpc: "2.0" or method |
-32601 | Method not found | Method name typo |
-32602 | Invalid params | Missing message, empty parts, or no messageId |
-32603 | Internal error | Server-side failure; data.code may say INSUFFICIENT_CREDITS |
-32001 | Task not found | Unknown taskId (also returned for another tenant’s task) |
-32002 | Task not cancelable | Task already finished |
-32003 | Push notifications unsupported | Not enabled for this agent |
-32004 | Unsupported operation | Unsupported protocol version, or A2A disabled on the agent |
-32005 | Content type not supported | A file part whose mimeType is not accepted |
-32007 | Extended card not configured | agent/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
-32603withdata.code = "INSUFFICIENT_CREDITS". - Idempotency is keyed on
messageIdper 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:
- Enable A2A on the agent and create an
a2a-scoped key. - Register the agent in the calling platform using its card URL,
https://your-deployment.qraptor.app/agents/{agentId}/.well-known/agent-card.json. - Set the credential to
Authorization: Bearer qr_…. - Invoke with
message/send. Most platforms read the answer fromresult.artifacts[*].parts[*].text. - If your agent can run long, confirm the platform tolerates a
workingresult and pollstasks/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:
| Legacy | Replacement |
|---|---|
POST /agents/{id}/tasks | message/send |
GET /agents/{id}/tasks/{taskId} | tasks/get |
GET /agents/{id}/tasks/{taskId}/events | message/stream |
POST /agents/{id}/tasks/{taskId}/cancel | tasks/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 sendkind. - Fields are camelCase (
contextId,artifactId,status.state), not snake_case (task_id,context_id).
Troubleshooting
| Symptom | Cause |
|---|---|
404 with "A2A is not enabled for this agent" | Turn on A2A Protocol in the agent’s settings. |
401 / 403 | Key missing, expired, revoked, or lacking the a2a scope. |
-32602 on a request that looks right | Check messageId is present and parts is non-empty. |
| The agent answers generically, as if it saw no input | Parts are probably malformed — confirm kind/text (or legacy type/content) spelling. |
| Client hangs, then times out | Long-running agent. Handle state working and poll tasks/get, or lower the blocking budget. |
Card fetch returns 401 | Discovery is public — you are probably requesting a path that is not one of the two card URLs. |
Empty history | Expected. Ask for it with configuration.historyLength or params.historyLength. |
-32602 on a file part | Missing mimeType, invalid base64, over 20 MB, more than 10 files, or a uri that is not public https. |
| Agent ignores an attached file | It has no tool that reads that type. Check the card’s defaultInputModes and attach a vision / file-parse tool to the agent. |
Related
- API access — keys, scopes, other endpoints
- Managing agents — enabling A2A, testing in Studio
- Multi-agent systems — qRaptor-internal agent collaboration