Integração A2A — Para Parceiros¶
O que é o protocolo A2A?¶
O Agent-to-Agent (A2A) é um protocolo que permite que seu agente de IA se comunique com o agente iFriend através de JSON-RPC 2.0 sobre HTTP.
Quando usar?¶
- Seu agente de IA precisa de informações de turismo que a iFriend possui
- Você quer que seus usuários façam reservas de experiências
- Você quer oferecer serviços da iFriend como parte do seu ecossistema
- Você quer integrar o iFriend com seu sistema de agentes (Agentforce, OpenAI Agents, LangGraph, CrewAI, Semantic Kernel, etc.)
Arquitetura de Segurança¶
Toda requisição (exceto o Agent Card, que é público) precisa estar autenticada por um destes dois modos:
| Modo | Para quem | O que enviar em cada requisição |
|---|---|---|
| API client iFriend (OAuth) | Integradores que já têm client_id, client_secret e integrator_id da API iFriend |
X-Client-Id, X-Client-Secret, X-Integrator-Id |
| JWT de conta de serviço | Parceiros com uma conta de serviço (email + senha) com a role ROLE_A2A_USER |
Authorization: Bearer <jwt> |
Partner Agent
|
| HTTPS + (X-Client-Id / X-Client-Secret / X-Integrator-Id)
| ou Authorization: Bearer <jwt>
v
Cloud Run (A2AAuthMiddleware)
| OAuth: troca as credenciais por um token no servidor OAuth da iFriend
| JWT: valida assinatura, expiração e role ROLE_A2A_USER
v
Google ADK Agent (iFriend) → API iFriend com a SUA credencial
- O endpoint A2A é exposto diretamente pelo serviço Cloud Run, protegido pelo
A2AAuthMiddleware. - O agente chama a API iFriend em nome da sua credencial: no modo OAuth com
X-API-TOKEN+X-Integrator-Id(o mesmo par que a sua integração já usa direto na API), no modo JWT com o seu token. - Se a requisição trouxer
Authorization: Bearere os headers OAuth, vale o JWT. - A conversa (
contextId) fica atrelada à credencial que a criou: outra credencial não consegue continuar a mesma sessão.
Quick Start¶
1. Receba as credenciais¶
Você vai precisar da URL do endpoint A2A e de uma das credenciais abaixo:
| Ambiente | A2A_URL |
|---|---|
| Produção | https://agents.theifriend.com/trip/a2a |
| Stage | https://stage.agents.theifriend.com/trip/a2a |
- Opção A — API client iFriend (recomendado para quem já integra com a API):
client_id,client_secreteintegrator_id. São as mesmas credenciais da sua integração com a API iFriend — não há cadastro extra para o A2A. Use as credenciais do ambiente correspondente (credenciais de stage só funcionam no stage). - Opção B — Conta de serviço: a equipe iFriend provisiona um email + senha com a role
ROLE_A2A_USER.
2. Obtenha o Agent Card (público, sem autenticação)¶
curl https://agents.theifriend.com/trip/a2a/.well-known/agent-card.json
Você receberá algo como:
{
"name": "ifriend_agent",
"version": "3.3.6",
"protocolVersion": "0.2.6",
"capabilities": {
"streaming": true,
"pushNotifications": false,
"extensions": [
{"uri": "https://theifriend.com/a2a/extensions/caller-context/v1", "required": false}
]
},
"defaultInputModes": ["text/plain", "application/json"],
"skills": [
{"id": "discovery", "name": "Busca de experiências e guias"},
{"id": "itinerary", "name": "Montagem de roteiros"},
{"id": "quote", "name": "Cotação de preços"},
{"id": "booking", "name": "Reservas"},
{"id": "payment", "name": "Pagamento"},
{"id": "support", "name": "Atendimento humano"},
{"id": "custom_tour", "name": "Tour personalizado"},
{"id": "booking_info", "name": "Consulta de reservas"}
]
}
Com o modo OAuth habilitado, o Agent Card anuncia os dois modos em securitySchemes/security
(bearerAuth ou clientId + clientSecret + integratorId, todos como headers).
3. Autentique¶
Opção A — API client iFriend (client_id / client_secret / integrator_id)¶
Não chame nenhum endpoint de token. Envie os três headers em toda requisição ao A2A; o servidor da iFriend
troca as credenciais por um access token no servidor OAuth (grant_type=client_credentials), guarda esse token em
cache (~1h) e renova sozinho:
X-Client-Id: SEU_CLIENT_ID
X-Client-Secret: SEU_CLIENT_SECRET
X-Integrator-Id: SEU_INTEGRATOR_ID
Não envie Authorization: Bearer junto — se ele estiver presente, a requisição é tratada como JWT.
Para conferir se as credenciais são válidas sem passar pelo A2A, chame o servidor OAuth direto — é o mesmo pedido que o agente faz por baixo:
# Produção: https://oauth.theifriend.com · Stage: https://stage.oauth.theifriend.com
curl -X POST https://oauth.theifriend.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=SEU_CLIENT_ID&client_secret=SEU_CLIENT_SECRET"
# → {"access_token": "...", "expires_in": 3599}
Opção B — JWT de conta de serviço¶
O mesmo endpoint de autenticação usado por toda a API iFriend — não é um mecanismo especial criado para A2A:
curl -X POST https://api.theifriend.com/authentication_token \
-H "Content-Type: application/json" \
-d '{"email": "SEU_EMAIL", "password": "SUA_SENHA"}'
Resposta:
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Sobre expiração: não há um TTL único documentado — trate o token como de vida curta/média e implemente reautenticação automática ao receber 401 Unauthorized, em vez de confiar num tempo fixo.
4. Envie uma mensagem¶
Opção A (API client iFriend):
curl -X POST https://agents.theifriend.com/trip/a2a/ \
-H "Content-Type: application/json" \
-H "X-Client-Id: SEU_CLIENT_ID" \
-H "X-Client-Secret: SEU_CLIENT_SECRET" \
-H "X-Integrator-Id: SEU_INTEGRATOR_ID" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": "msg-001",
"parts": [{"kind": "text", "text": "Quero experiências em São Paulo"}]
}
}
}'
Opção B (JWT):
curl -X POST https://agents.theifriend.com/trip/a2a/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SEU_TOKEN" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "message/send",
"params": {
"message": {
"role": "user",
"messageId": "msg-001",
"parts": [{"kind": "text", "text": "Quero experiências em São Paulo"}]
}
}
}'
messageId é obrigatório (gere um UUID por mensagem). Para continuar uma conversa multi-turn, inclua "contextId" (retornado na resposta anterior) e, se quiser continuar uma task específica, "taskId" — ambos dentro do objeto message, não em params. Mande a mesma credencial em todos os turnos da conversa.
5. Receba a resposta¶
{
"jsonrpc": "2.0",
"id": "1",
"result": {
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"contextId": "a1b2c3d4-...",
"kind": "task",
"status": {"state": "completed"},
"artifacts": [
{
"artifactId": "…",
"parts": [{"kind": "text", "text": "Encontrei estas experiências em São Paulo:\n1. …"}]
}
],
"history": ["…"]
}
}
A resposta é um objeto Task (kind: "task") — o id é o identificador da task (use em tasks/get/tasks/cancel como id, não taskId). O texto final do agente vem em artifacts[].parts; com a task completed, status.message vem vazio. history traz as mensagens intermediárias (inclusive chamadas de ferramentas como DataPart) e não deve ser repassado ao cliente final.
A resposta é só texto¶
O Trip trata toda chamada A2A como um canal só texto, porque o chamador repassa a resposta ao cliente (ex: WhatsApp):
- Nada de cards, botões ou formulários. Produtos e opções vêm numa lista numerada no próprio texto, com o nome, uma descrição curta, o preço e o link quando houver. O cliente pode responder com os números.
- O Trip não promete ação futura ("aguarde", "estou gerando") — cada resposta é completa.
- Sem PDF e sem anexos. Quando o orçamento precisa ser finalizado, o Trip responde com um resumo completo do pedido e informa que o time vai montar a proposta e enviar; o encaminhamento fica com o chamador.
- O raciocínio interno do modelo nunca é enviado.
Exceção: busca de produto com a extensão generative-ui/v1 (opcional). Se o seu cliente sabe
renderizar cards, peça a extensão https://theifriend.com/a2a/extensions/generative-ui/v1 (ver
abaixo) — a busca de produto volta a vir também como blocks (cards), além do texto. As demais
funcionalidades (cotação, orçamento, anexo, formulário) continuam só em texto.
6. (Opcional) Reative os cards de busca de produto¶
Por padrão a busca de produto no A2A vem só como texto (ver acima). Para recebê-la também como cards,
peça a extensão generative-ui/v1 no header X-A2A-Extensions, em toda mensagem da conversa
(não é "sticky" por task — se parar de mandar o header, a resposta volta a ser só texto):
X-A2A-Extensions: https://theifriend.com/a2a/extensions/generative-ui/v1
Com a extensão pedida, a busca de produto chega como um DataPart em Task.history (não em
Task.artifacts, que continua só com o texto final do agente) — uma mensagem role: "agent" com uma
part kind: "data" cujo data é a resposta da ferramenta serializada: data.response.__ui_blocks__
(sempre true), data.response.blocks (os cards) e data.response.products (lista plana dos
produtos). Esse é o mesmo formato usado hoje no SSE do webchat.
Para pedir as duas extensões ao mesmo tempo (contexto do chamador + cards), separe por vírgula:
X-A2A-Extensions: https://theifriend.com/a2a/extensions/caller-context/v1,https://theifriend.com/a2a/extensions/generative-ui/v1
7. (Opcional) Envie o contexto do chamador¶
O protocolo A2A não define um formato para "contexto antes da mensagem". O Trip publica o próprio
contrato no Agent Card: a extensão https://theifriend.com/a2a/extensions/caller-context/v1, opcional.
Ela serve para quem chama o Trip em nome de um cliente final, como um agente de CRM no WhatsApp. O schema
completo está em capabilities.extensions[0].params do Agent Card.
Contexto do chamador (extensão caller-context/v1)¶
Envie o contexto em um destes lugares, em ordem de precedência:
- DataPart em
message.parts:{"kind": "data", "data": {...}}. Preferido. message.metadata, com as chaves soltas ou aninhadas sob a URI da extensão (também aceitos como chave de aninhamento:caller_context,callerContext,context,contexto).params.metadata(mesmas regras do item 2).
{
"jsonrpc": "2.0", "id": "1", "method": "message/send",
"params": {
"message": {
"role": "user", "messageId": "msg-002", "contextId": "sessao-crm-123",
"parts": [
{"kind": "text", "text": "Cliente quer passeio em Lisboa dia 12/11"},
{"kind": "data", "data": {
"caller": "agentforce",
"caseId": "500…", "contactId": "003…", "accountId": "001…", "leadId": "00Q…",
"canal": "WhatsApp", "idioma": "pt-BR",
"cliente": {"nome": "Maria", "email": "maria@exemplo.com", "telefone": "5511999990000"},
"conta": {"tipo": "viajante", "existente": true}
}}
]
}
}
}
| Campo | Aliases aceitos | Uso no Trip |
|---|---|---|
caller |
origem, source, agent |
Identifica o agente chamador |
case_id, contact_id, account_id, lead_id |
caseId, contactId, accountId, leadId |
Rastreio (IDs do CRM) |
canal |
channel |
Canal do cliente final |
idioma |
language, locale, lang |
Idioma da resposta |
cliente.{nome,email,telefone} |
customer/contact, name, phone, whatsapp; na raiz: customer_name, customer_email, customer_phone |
O Trip não pede de novo esses dados |
conta.{tipo,existente,empresa,cnpj} |
account, type/account_type, exists, company |
tipo: viajante ou agencia |
Como o Trip se comporta:
- Sem contexto, o Trip funciona normalmente, só com o texto.
- Basta enviar o contexto no 1º turno. Os turnos seguintes da mesma conversa (
contextId) sem contexto mantêm o que já foi recebido; um turno com contexto novo substitui o anterior. - Campos desconhecidos no DataPart (ou aninhados sob uma das chaves do item 2) são guardados (
extra) e não causam erro. Chaves desconhecidas soltas emmetadatasão ignoradas, porque metadata costuma trazer ruído de orquestração. - Quando chamado via A2A, o Trip não faz handoff para atendimento humano, porque quem qualifica e encaminha é o agente chamador. Se não houver produto, ele responde isso claramente.
- Formato protobuf/A2A v1 (
"role": "ROLE_USER",content: [...], parts semkind, métodosSendMessage/SendStreamingMessage): aceito. O Trip normaliza para o formato v0.3 antes da validação.
Métodos disponíveis¶
Todos são chamados via JSON-RPC 2.0 (POST no mesmo endpoint /, método indicado no campo "method" do corpo):
| Método | Descrição |
|---|---|
message/send |
Envia mensagem e inicia/continua uma task (multi-turn) |
message/stream |
Igual a message/send, mas com resposta em streaming (SSE) |
tasks/get |
Consulta o resultado de uma task (params: {"id": "<task_id>"}) |
tasks/cancel |
Cancela uma task em andamento (params: {"id": "<task_id>"}) — cancelamento é cooperativo: nunca falha, mas não interrompe instantaneamente uma resposta já em geração |
Endpoints¶
| Endpoint | Método | Descrição |
|---|---|---|
.../a2a/.well-known/agent-card.json |
GET | Agent Card (público, sem auth) |
.../a2a/ |
POST | Todos os métodos JSON-RPC acima |
Não há endpoints REST separados por task (ex: GET /tasks/{id}) — tudo é JSON-RPC no mesmo endpoint.
Erros de autenticação¶
| HTTP | Corpo (error) |
Causa |
|---|---|---|
| 401 | Missing header(s): X-Client-Secret, X-Integrator-Id |
Modo OAuth: veio X-Client-Id mas faltou algum dos outros headers |
| 401 | Invalid client credentials |
Modo OAuth: o servidor OAuth recusou client_id/client_secret (confira também se são do ambiente certo) |
| 503 | Authentication service unavailable |
Modo OAuth: o servidor OAuth da iFriend não respondeu — tente de novo |
| 401 | Invalid or expired JWT token |
Modo JWT: token inválido ou expirado — reautentique |
| 403 | Forbidden: role 'ROLE_A2A_USER' required |
Modo JWT: a conta não tem a role ROLE_A2A_USER |
| 401 | Missing or invalid Authorization header |
Nenhuma credencial reconhecida (sem Bearer e sem X-Client-Id) |
Exemplos de Código¶
Consulte Exemplos para código em Python, JavaScript e cURL, e os scripts executáveis em examples/a2a_test_cli.py, examples/a2a_echo_agent.py e examples/a2a_outbound_roundtrip_test.py no repositório da iFriend para testar localmente. A coleção Postman ifriend-agents-a2a.postman_collection.json tem a request "Enviar Mensagem (OAuth integrador)" (variáveis client_id, client_secret, integrator_id).
Suporte¶
Em caso de dúvidas: dev@theifriend.com