A2A Reference
How an external client talks to a deployed Coelevate agent over the A2A protocol — authentication, requests, task states and how to answer an agent's questions.
A deployed Coelevate agent speaks A2A, the open agent-to-agent protocol. Use it to run an agent from your own code: authenticate with an API key, send a request, follow the task as it works, answer if it asks you something, and read the result.
Everything below was checked against the platform's own code and the A2A SDK it is built on. The examples use shell variables so you can paste them as they are:
export API_URL="<the agent address your key panel shows>" # no trailing slash
export TOKEN_URL="<the token address your key panel shows>"
export CLIENT_ID="<your key id>"
export CLIENT_SECRET="<your key secret>"1. Authenticate
Create an API key with the Agent execution permission for the project. You receive the key id and secret once; keep them out of source control. Exchange them for a short-lived bearer token with the standard OAuth 2.0 client-credentials grant:
curl -sS -X POST "$TOKEN_URL" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials"{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 600,
"scope": "a2a:invoke",
"project_id": "proj_…"
}- Send the key as HTTP Basic, as above, or as the form fields
client_idandclient_secret— not both. - The token lasts 600 seconds and there is no refresh token. Exchange the key again shortly
before the token's own
expclaim. - The only scope is
a2a:invoke. It opens the agents of the key's project and nothing else. project_idcomes back in the reply. You never send it to the token endpoint.
Send the token on every call as Authorization: Bearer <access_token>.
Token-endpoint failures follow RFC 6749 ({"error": "...", "error_description": "..."}):
| Status | error | Meaning |
|---|---|---|
| 401 | invalid_client | The key id or secret is wrong, or the key was revoked. |
| 400 | invalid_request | A required field is missing or both credential forms were sent. |
| 400 | unsupported_grant_type | Only client_credentials is accepted. |
| 400 | invalid_scope | A scope the key does not hold was requested. |
| 503 | temporarily_unavailable | Try again with a short back-off. |
Revoking a key stops new tokens; a token already issued stays valid until it expires.
2. Find an agent
List the agents deployed in your project:
curl -sS "$API_URL/agents" -H "Authorization: Bearer $TOKEN"Each entry has an id and a card_url. Only deployed agents are listed; an agent that is
not deployed answers 404 for its card, its contract and any new session.
No address names a project: your access token does. The same URL serves every project, and the token decides which one a call is for:
| What | Address |
|---|---|
| Agent directory | GET /agents |
| Agent card | GET /agents/{agent_id}/.well-known/agent-card.json |
| Contract | GET /agents/{agent_id}/contract |
| Every A2A call | POST /agents/{agent_id}/ |
Keep the trailing slash on the POST address. A redirect would drop the request body.
The agent card is a standard A2A AgentCard: the agent's name and description, the one
skill it offers, its capabilities (streaming is supported), the security schemes it accepts
(HTTP bearer, and OAuth 2.0 client credentials with the a2a:invoke scope) and the extensions
it uses.
The contract tells you what to send. request.input_schema is a JSON Schema for the
params of a request (below); a request that does not satisfy it is rejected. Read it each
time rather than caching it: it describes the version of the agent that is deployed now.
3. Send a message
Every call is one JSON-RPC 2.0 POST to the agent's address. The method in the body picks
the operation:
method | Use |
|---|---|
message/send | Send a message; the reply comes back when the agent next needs you or finishes. |
message/stream | The same, as a stream of task updates. |
tasks/get | Read a task's current state. |
tasks/cancel | Stop a running task. |
tasks/resubscribe | Re-attach to the stream of a task that is still running. |
tasks/get, tasks/cancel and tasks/resubscribe keep working for a task that already exists
even if its agent has since been undeployed, and on an archived project tasks/get and
tasks/cancel still answer while message/send is refused.
A message has two forms, chosen by its part type. Do not mix them in one message.
A question: a text part
A message of one text part is a conversation turn. The agent answers directly with a message, and no task is created. Use it to ask what the agent can do.
curl -sS -X POST "$API_URL/agents/$AGENT_ID/" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "message/send",
"params": {
"message": {
"kind": "message",
"messageId": "11111111-1111-4111-8111-111111111111",
"role": "user",
"parts": [{"kind": "text", "text": "What can you do?"}]
}
}
}'The reply carries a contextId. Send it back on later messages to stay in the same
conversation.
A request: a data part
To have the agent do its work, send one data part whose data is an envelope of a type
and a payload. For a new request, type is new_request and the payload carries your
free-text message, the agent's id, and params that satisfy the contract's input_schema:
{
"jsonrpc": "2.0",
"id": "req-2",
"method": "message/send",
"params": {
"message": {
"kind": "message",
"messageId": "33333333-3333-4333-8333-333333333333",
"role": "user",
"contextId": "ctx-123",
"parts": [
{
"kind": "data",
"data": {
"type": "new_request",
"payload": {
"message": "Qualify this lead and tell me what to do next.",
"agent_id": "lead_qualification",
"params": { "…": "as the contract's input_schema requires" }
}
}
}
]
}
}
}Put the free text inside the payload as message; a separate text part next to the data
part is rejected rather than silently dropped.
Rules for the fields you choose:
messageId— a fresh UUID for every message.contextId— optional. Omit it and one is created and returned. If you supply one it must be a non-blank printable string of at most 256 characters; anything else is rejected with JSON-RPC error-32602. Wait for a reply before sending the next message on the samecontextId.X-Persona-Id(header) — optional. It selects the role the agent acts for; it is not a credential. Leave it off to use the project's default.
The agent can also be told whom it acts for and which context to use; both travel as metadata
on the message, using the extension addresses listed on the agent card (identity-claims and
context-selection).
4. Follow the task
A request creates a task. Its status.state moves through:
| State | Meaning |
|---|---|
submitted | Accepted. |
working | In progress. A sub_status in the update's metadata names the phase. |
input-required | The agent needs you; see below. Not final. |
completed | Finished; the result was delivered. Final. |
failed | Could not be completed. Final. |
canceled | Stopped by a tasks/cancel. Final. |
A run that has produced its result — whether it succeeded or not — pauses at
input-required and asks for feedback before the task reaches its final state. That is when
the result is available, as described next.
Read the result
The result is the task's response artifact. Its first part is a text part with the
agent's reply. Its second is a data part:
{ "status": "success", "output": { "…": "…" }, "deliveries": [], "issues": [] }status tells you how far to trust it: success is safe to use automatically, partial is
for a person to review first, and failed means do not use the output.
Answer the agent
A task in input-required carries a data part on its status message whose type says what is
being asked:
type | Asks for | Answer with |
|---|---|---|
question_request | A piece of information (question, optional suggestions). | question_response |
confirmation_request | Approval to proceed (summary, risks). | confirmation_response |
closure_review | Feedback on the finished result. | closure |
Answer by sending a new message/send with the same contextId and taskId and one
data part of the matching type. A text reply to a paused task is not an answer; it is treated
as a new conversation turn and the run does not advance.
{
"jsonrpc": "2.0",
"id": "req-3",
"method": "message/send",
"params": {
"message": {
"kind": "message",
"messageId": "22222222-2222-4222-8222-222222222222",
"role": "user",
"contextId": "ctx-123",
"taskId": "task-456",
"parts": [
{
"kind": "data",
"data": { "type": "question_response", "payload": { "answer": "Yes, the budget is approved." } }
}
]
}
}
}| Answer | payload |
|---|---|
question_response | {"answer": "…"} — answer may be null to decline. |
confirmation_response | {"confirmed": true, "comments": "…"} — comments is optional. |
closure | {"feedback": {"verdict": "success", "sentiment": "…", "comments": "…", "improvement_suggestions": ["…"]}} — every field is optional. verdict is success, partial or failure. |
A question or confirmation can carry an expires_at time. Answering after it fails the
session instead of resuming it.
Closing the task with closure is what moves it to its final state, and it is also how your
verdict reaches the agent's learning loop: failure feedback is the most useful signal you can
give.
Read or cancel a task
{ "jsonrpc": "2.0", "id": "req-4", "method": "tasks/get", "params": { "id": "task-456" } }{ "jsonrpc": "2.0", "id": "req-5", "method": "tasks/cancel", "params": { "id": "task-456" } }Errors
| Status | When | Body or header |
|---|---|---|
| 401 | No token | missing platform token, with WWW-Authenticate: Bearer |
| 401 | Expired or invalid token | invalid or expired platform token |
| 401 | Not a bearer token | malformed bearer token |
| 403 | The token lacks the scope | this token does not carry the a2a:invoke scope |
| 403 | A back-office key was used | A back-office key cannot invoke agents; create an agent-execution key and exchange that. |
| 404 | The agent is not deployed, or the token belongs to a different project | agent_not_found for an undeployed agent |
| 413 | The request body is too large | The limit is just under 1 MB. |
Protocol-level problems — an invalid contextId, a message that mixes part types, params
that do not satisfy the contract — come back as JSON-RPC errors in the body of a 200
response, not as HTTP errors.
Limits
- Tokens last 600 seconds and cannot be refreshed; exchange the key again.
- Request bodies are limited to just under 1 MB.
- A key lasts 90 days by default.
- No request-rate limit is documented yet. Back off and retry on
503.