Skip to content

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: Bearer e 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_secret e integrator_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:

  1. DataPart em message.parts: {"kind": "data", "data": {...}}. Preferido.
  2. 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).
  3. 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 em metadata sã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 sem kind, métodos SendMessage/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