# Connecting Albato Copilot to Your Agent over A2A — Albato Help Center

Source: https://albato.com/wiki/articles/connecting-albato-copilot-to-your-agent-over-a2a

Welcome to Albato Help

Search articles

# Connecting Albato Copilot to Your Agent over A2A

Last update:  
16/9/2026 · 10 min read

* * *

IN THIS ARTICLE

A guide for developers at Albato embedded partners. It explains how your AI agent can delegate work to Albato Copilot over the open A2A protocol (Agent2Agent, version 1.0) to have it build an automation and show the result to the user inside your own product.

## What Copilot Is over A2A

Albato Copilot is an agent that, on behalf of an Albato user:

-   builds, configures, validates and starts automations (bundles): picks apps, triggers and actions, connections, maps fields;
-   searches the Albato catalog for apps, their triggers and actions, and lists the user's connections;
-   inspects existing bundles and explains why they are not running.

Over A2A your agent sends Copilot a task as plain text in the user's language and receives a stream of events back: what Copilot is doing right now, the questions it has, and the final answer. Technically it is JSON-RPC 2.0 over HTTPS with Server-Sent Events (SSE) for streaming. The official specification and SDKs live at [a2a-protocol.org](https://a2a-protocol.org/); the Python package is **a2a-sdk**.

Key concepts:

| Term | Meaning |
| --- | --- |
| Agent Card | JSON description of the agent at **<COPILOT\_URL>/.well-known/agent-card.json**: skills, JSON-RPC address, auth requirements. The SDK reads it for you |
| Task | One unit of work (one turn of the conversation). Has a **taskId** and a state: **SUBMITTED** → **WORKING** → **COMPLETED**, **FAILED**, **CANCELED**, or **INPUT\_REQUIRED** (Copilot is waiting for an answer) |
| contextId | Conversation identifier. Send it with every following message and Copilot remembers what has already been done |
| Artifact | The task result. For Copilot it is the **answer** artifact: the answer text and, if a bundle was created, its **bundle\_id** |

## What Your Albato Manager Provides

1.  **The Copilot address**, a base URL, referred to below as **<COPILOT\_URL>**.
2.  **A partner token** that looks like cpa2a\_…, a secret that identifies your agent. One per partner. Keep it on your server like any API key and never send it to a browser. If it is exposed, contact your manager: it will be reissued and the old one stops working immediately.

## Authentication: Two Tokens on Every Request

Every request to **<COPILOT\_URL>/a2a** must carry two headers:

| Header | What it is | Where it comes from |
| --- | --- | --- |
| **X-Copilot-Partner-Token: cpa2a\_…** | **Who is asking**: your agent | Issued by your Albato manager |
| **Authorization: Bearer <JWT>** | **On whose behalf**: the Albato user Copilot works for | That user's Albato session token (see section 4) |

Plus the protocol header **A2A-Version: 1.0** (SDK clients add it automatically; add it yourself for manually configured requests).

Optional: if the user works inside an Albato Teams workspace, add the **WorkspaceId** and **SpaceId** headers, the same ones your UI sends to the Albato API.

The partner token never substitutes for the user token: without the user's token Copilot cannot do anything in their account. There is no anonymous mode.

## Getting the User Token and Passing It to Copilot

Copilot accepts the same Albato session token you already use to show the Albato UI inside your product: the **session\_token** passed to the Albato UI when it opens (stored in the browser in the **authToken\_<env>** cookie). No separate Copilot token is needed.

Recommended layout:

`user's browser                            your backend (the agent lives here)              Albato Copilot   user types in chat  →  your API (your own authentication)                       looks up this user's Albato session token, the one                       you received from Albato when you opened the                       Albato UI for them                       →  POST <COPILOT_URL>/a2a                           X-Copilot-Partner-Token: cpa2a_…  (server secret)                           Authorization: Bearer <user's session_token>                           A2A-Version: 1.0                      →  Copilot   Copilot answers/questions  ←  your API streams them to your UI      ←  event stream`

Rules:

-   **Run the A2A client on your server.** The partner token must not reach the browser. If your agent runs in the frontend, put your own proxy between it and Copilot that adds the partner token.
-   **Your backend holds the user token**, not the page. Your frontend authenticates against your API as usual and the backend attaches the Albato token itself, so it never ends up in JavaScript or browser logs. If your architecture does pass it from the frontend, send it only in the body or a header of an HTTPS request to your own API, never in a URL.
-   **Lifetime** equals the user's Albato session. If Copilot answers **401** for the user token, obtain a fresh one the same way you do for the Albato UI and retry.
-   One user token, one user. Never send one user's tasks with another user's token: Copilot would act in the wrong account.

## Step by Step

### Step 1. Read the Agent Card

`curl -s <COPILOT_URL>/.well-known/agent-card.json`

The card is public. You need the JSON-RPC address (**supportedInterfaces\[0\].url**, which is **<COPILOT\_URL>/a2a**) and the list of skills. The SDK does this for you.

### Step 2. Send a Task

Use the streaming method **SendStreamingMessage**: you see progress immediately and the connection does not hit load balancer timeouts. The blocking **SendMessage** keeps the HTTP connection open until the task ends (which can take minutes) and is only suitable for short requests.

`curl -N -s -X POST <COPILOT_URL>/a2a \     -H "Content-Type: application/json" \     -H "Accept: text/event-stream" \     -H "A2A-Version: 1.0" \     -H "X-Copilot-Partner-Token: $COPILOT_PARTNER_TOKEN" \     -H "Authorization: Bearer $ALBATO_USER_TOKEN" \     -d '{       "jsonrpc": "2.0", "id": "1", "method": "SendStreamingMessage",       "params": {"message": {"messageId": "m-1", "role": "ROLE_USER",                              "parts": [{"text": "Which triggers does Telegram have?"}]}}     }'`

**messageId** is any unique identifier on your side (a UUID, for example).

### Step 3. Read the Event Stream

Each SSE event is a **data: {...}** line with a JSON-RPC response whose **result** is one of:

| Event | What to do |
| --- | --- |
| **task** | Task started. Store **task.id** (taskId) and **task.contextId** |
| **statusUpdate** with **state: TASK\_STATE\_WORKING** | Progress. **status.message.parts\[0\].text** holds a short line ("Calling search\_partners", "step\_2: completed …"), the second part is a DataPart with details. Use this for a progress indicator |
| **artifactUpdate** | The final answer: **artifact.name == "answer"**, text in **parts\[0\].text**, a DataPart may carry the **bundle\_id** of the created bundle |
| **statusUpdate** with **state: TASK\_STATE\_COMPLETED** | Done. The answer text is repeated in **status.message** |
| **statusUpdate** with **state: TASK\_STATE\_INPUT\_REQUIRED** | Copilot is waiting for the user, see step 4 |
| **statusUpdate** with **state: TASK\_STATE\_FAILED** | Error: text in **status.message**, DataPart with **error\_code** and **recoverable** |
| **statusUpdate** with **state: TASK\_STATE\_CANCELED** | The task was canceled |

The stream ends when the task finishes or reaches **INPUT\_REQUIRED**.

### Step 4. Copilot's Questions (INPUT\_REQUIRED)

Copilot often asks for clarification: which action to use, which connection, what exactly to build. The task then moves to **TASK\_STATE\_INPUT\_REQUIRED** and **status.message** contains:

-   the question text, **parts\[0\].text**;
-   a DataPart with details, **parts\[1\].data**:
    -   **{"type": "question"}**, a free-form question, answer with text;
    -   **{"type": "hitl\_request", "hitl\_type": "select" | "add\_connection" | "connection\_actions" | …, "prompt": "…", "options": \[{"option\_id": "…", "label": "…"}\], "connection\_url": "…"?}**, a choice between options.

Show the question and options to the user and send their answer as a new message with the same **taskId** and **contextId**:

`{"jsonrpc": "2.0", "id": "2", "method": "SendStreamingMessage",    "params": {"message": {"messageId": "m-2", "role": "ROLE_USER",                       "taskId": "<taskId>", "contextId": "<contextId>",                       "parts": [{"text": "My Telegram bot"}]}}}`

For options you may send the **option\_id**, its **label** (case-insensitive), or a separate part **{"data": {"selected\_option\_id": "cred\_1"}}**. If the answer matches no option, the task stays in **INPUT\_REQUIRED** and the message repeats the list of options.

A special case is **add\_connection**: the user has to connect an app. The DataPart carries a **connection\_url**. Open it for the user in a browser (new window), wait until they authorize, then reply with the **add\_connection** option. Copilot then offers **connection\_actions** with **continue** and **try\_again**. This step cannot be automated: a human performs it.

### Step 5. Continue the Conversation

-   Next message in the same conversation: pass the **contextId** from the first reply (without **taskId**). Copilot keeps the context: apps found, the bundle being built, earlier answers.
-   A new message without **taskId** while the previous task is unfinished: the running task is interrupted, an unanswered question is canceled, and a new task starts.
-   Cancel a task: **{"method": "CancelTask", "params": {"id": "<taskId>"}}**.
-   Check a task: **{"method": "GetTask", "params": {"id": "<taskId>"}}**.
-   A message to a finished task is rejected, start a new one with the same **contextId**.

## Python Example (a2a-sdk)

`pip install "a2a-sdk>=1.1" httpx`

`import asyncio   import os      import httpx   from a2a.client import ClientConfig, create_client   from a2a.helpers import (       get_data_parts, get_stream_response_text,       new_data_part, new_message, new_text_part,   )   from a2a.types import Role, SendMessageRequest, TaskState      COPILOT_URL = os.environ["COPILOT_URL"]              # from your manager   PARTNER_TOKEN = os.environ["COPILOT_PARTNER_TOKEN"]  # server secret         async def ask_copilot(user_token, text, *, context_id=None, task_id=None,                            selected_option_id=None):       http = httpx.AsyncClient(           headers={               "X-Copilot-Partner-Token": PARTNER_TOKEN,               "Authorization": f"Bearer {user_token}",           },           timeout=httpx.Timeout(600, connect=10),       )       config = ClientConfig(streaming=True, httpx_client=http)       client = await create_client(COPILOT_URL, client_config=config)          parts = [new_text_part(text)] if text else []       if selected_option_id:           parts.append(new_data_part({"selected_option_id": selected_option_id}))       message = new_message(parts, context_id=context_id, task_id=task_id,                         role=Role.ROLE_USER)          state, answer, question = None, "", None       async for chunk in client.send_message(SendMessageRequest(message=message)):           if chunk.HasField("task"):               task_id, context_id = chunk.task.id, chunk.task.context_id           elif chunk.HasField("status_update"):               status = chunk.status_update.status               state = TaskState.Name(status.state)               if state == "TASK_STATE_WORKING":                   progress = get_stream_response_text(chunk)                   if progress:                       print("[copilot]", progress)  # progress               elif state == "TASK_STATE_INPUT_REQUIRED":                   datas = get_data_parts(status.message.parts)                   question = {                       "text": get_stream_response_text(chunk),                       "data": datas[0] if datas else None,                   }           elif chunk.HasField("artifact_update"):               answer += get_stream_response_text(chunk)          await client.close()       await http.aclose()       return {"state": state, "answer": answer, "question": question,           "task_id": task_id, "context_id": context_id}         async def main():       user_token = "<the Albato user's session_token>"       r = await ask_copilot(user_token, "Create an automation: new lead in amoCRM "                                  "-> message in Telegram")       while r["state"] == "TASK_STATE_INPUT_REQUIRED":           print("Copilot asks:", r["question"]["text"])           for opt in (r["question"]["data"] or {}).get("options", []):               print("  -", opt["option_id"], ":", opt["label"])           reply = input("User's answer: ")           r = await ask_copilot(user_token, reply,                             context_id=r["context_id"], task_id=r["task_id"])       print(r["state"], r["answer"])         asyncio.run(main())`

Use one **httpx.AsyncClient** per user conversation: it keeps cookies and the connection, which makes requests faster and lets the load balancer keep the conversation on one server.

## Errors

HTTP errors arrive before JSON-RPC:

| Code | Cause | What to do |
| --- | --- | --- |
| **401** | Missing partner token, or missing/expired user token (**detail** says which) | Check the headers; refresh the user token |
| **403** | Partner token unknown or revoked | Contact your manager |

JSON-RPC errors arrive with HTTP 200 in the **error** field:

| Code | Meaning |
| --- | --- |
| **\-32602** | Invalid params: empty message, foreign or non-UUID **contextId**, **contextId** not matching the task, message to a finished task |
| **\-32001** | Task not found (unknown or stale **taskId**), start a new task with the same **contextId** |
| **\-32009** | Missing **A2A-Version: 1.0** header |
| **\-32603** | Internal Copilot error, retry later |

Copilot's own failures (no access to the Albato API, account limit reached, agent error) arrive as **TASK\_STATE\_FAILED** with a user-facing text.

## Limits and Tips

-   One task may run for up to 15 minutes; after that it is interrupted and returned as **FAILED**.
-   Tasks within one conversation (**contextId**) are sequential: wait for completion or **INPUT\_REQUIRED** before sending the next message.
-   Write to Copilot in the user's language, the answer comes back in the same language.
-   Chats of A2A sessions will be available in the Albato partner dashboard.
-   Do not log tokens. Keep the partner token on the server and the user token only in request memory.

For help, contact your Albato manager or the Albato support team.

Did this answer your question?
