For the complete documentation index, see llms.txt.

Welcome to Albato Help

Connecting Albato Copilot to Your Agent over A2A


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; the Python package is a2a-sdk.

Key concepts:

TermMeaning
Agent CardJSON description of the agent at <COPILOT_URL>/.well-known/agent-card.json: skills, JSON-RPC address, auth requirements. The SDK reads it for you
TaskOne unit of work (one turn of the conversation). Has a taskId and a state: SUBMITTEDWORKINGCOMPLETED, FAILED, CANCELED, or INPUT_REQUIRED (Copilot is waiting for an answer)
contextIdConversation identifier. Send it with every following message and Copilot remembers what has already been done
ArtifactThe 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:

HeaderWhat it isWhere it comes from
X-Copilot-Partner-Token: cpa2a_…Who is asking: your agentIssued by your Albato manager
Authorization: Bearer <JWT>On whose behalf: the Albato user Copilot works forThat 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:

EventWhat to do
taskTask started. Store task.id (taskId) and task.contextId
statusUpdate with state: TASK_STATE_WORKINGProgress. 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
artifactUpdateThe 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_COMPLETEDDone. The answer text is repeated in status.message
statusUpdate with state: TASK_STATE_INPUT_REQUIREDCopilot is waiting for the user, see step 4
statusUpdate with state: TASK_STATE_FAILEDError: text in status.message, DataPart with error_code and recoverable
statusUpdate with state: TASK_STATE_CANCELEDThe 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:

CodeCauseWhat to do
401Missing partner token, or missing/expired user token (detail says which)Check the headers; refresh the user token
403Partner token unknown or revokedContact your manager

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

CodeMeaning
-32602Invalid params: empty message, foreign or non-UUID contextId, contextId not matching the task, message to a finished task
-32001Task not found (unknown or stale taskId), start a new task with the same contextId
-32009Missing A2A-Version: 1.0 header
-32603Internal 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?