Confer — API specification

Defines all APIs for client ↔ server and server ↔ A2A peer.

General conventions

{
  "error": {
    "code": "invalid_request",
    "message": "Human-readable message",
    "details": { /* optional */ }
  }
}

Authentication

Client API (used by the user client)

Authentication

POST   /api/v1/auth/register
POST   /api/v1/auth/login
POST   /api/v1/auth/refresh
POST   /api/v1/auth/logout
POST   /api/v1/auth/oauth/{provider}    # OAuth callback

POST /api/v1/auth/login request:

{
  "username": "laowang",
  "password": "...",
  "device_id": "ios-abc123",
  "device_info": { "platform": "ios", "model": "iPhone 15", "os": "17.1" }
}

Response:

{
  "access_token": "eyJ...",
  "refresh_token": "...",
  "expires_in": 900,
  "user": { /* User object */ }
}

User and Agent configuration

GET    /api/v1/users/me
PATCH  /api/v1/users/me
GET    /api/v1/agents/me
PATCH  /api/v1/agents/me
PUT    /api/v1/agents/me/policies
PUT    /api/v1/agents/me/llm-keys      # 加密存储 LLM API keys

Contacts / Peer Agents

GET    /api/v1/contacts                     # 列出联系人
POST   /api/v1/contacts                     # 添加联系人
GET    /api/v1/contacts/{contact_id}
DELETE /api/v1/contacts/{contact_id}
PATCH  /api/v1/contacts/{contact_id}        # 修改 alias, tags, pinned 等

POST   /api/v1/contacts/lookup              # 按 DID / 域名 / username 查找

POST /api/v1/contacts/lookup request:

{
  "method": "domain",          // domain | did | username | qr_code | phone
  "value": "abc-industries.com"
}

Response: returns the list of candidate Agents found. lookup persists the discovered peers into peer_agents and includes the local id (peer_id) on each candidate — POST /api/v1/contacts uses exactly this id to add a contact. POST /contacts is idempotent: adding the same peer again returns the existing contact (200) rather than an error.

Adding a contact is the receiving party granting the other side consent to “consume my Agent”: only a peer who has been added as a contact can trigger my Agent to answer (consuming my LLM budget). An A2A message sent by an unconnected peer is held as a pending connection request — see “Connection consent gate” in 03-protocol.md.

POST   /api/v1/contacts/{contact_id}/policies   # 设置 standing policies

Conversations

GET    /api/v1/conversations                       # 列出我的对话(首页用)
POST   /api/v1/conversations                       # 创建新对话
GET    /api/v1/conversations/{id}
PATCH  /api/v1/conversations/{id}
DELETE /api/v1/conversations/{id}

GET    /api/v1/conversations/{id}/messages         # 分页:?before=&limit=
POST   /api/v1/conversations/{id}/messages         # 发消息
GET    /api/v1/conversations/{id}/messages/{msg_id}/stream    # SSE 流式接收 LLM 回复

POST   /api/v1/conversations/{id}/participants     # 加入 participant
DELETE /api/v1/conversations/{id}/participants/{p_id}

POST   /api/v1/conversations/{id}/read             # 标记已读

POST /api/v1/conversations/{id}/messages request:

{
  "content_type": "text",
  "content": "X100 寄存器 0x40 用什么功能码?",
  "in_reply_to": null,
  "via": "web"
}

Response:

{
  "id": "01HXKQ...",
  "delivery_status": "queued",
  "stream_url": "/api/v1/conversations/01HX.../messages/01HXK.../stream"
}

Permission management

GET    /api/v1/permissions/pending               # 待处理的 L2/L3 请求
POST   /api/v1/permissions/{id}/decide           # 批准/拒绝
GET    /api/v1/permissions/history               # 历史记录

POST /api/v1/permissions/{id}/decide request:

{
  "decision": "allow_always",       // allow_once | allow_always | deny | deny_always
  "scope": "peer_action"            // 限定范围
}

Among pending requests, those with action='connect' are connection requests (generated by inbound A2A when a stranger peer first makes contact). Approval (allow_*) writes the peer into peer_contacts, establishing the connection; a rejection does not. GET /pending attaches a description to each request (including the initiator and the first message) to help the owner make a decision.

GET    /api/v1/projects/{project_id}/peers              # 该项目下注册的 peer
POST   /api/v1/projects/{project_id}/peers
GET    /api/v1/projects/{project_id}/peers/{peer_id}/facts
PUT    /api/v1/projects/{project_id}/peers/{peer_id}/facts
GET    /api/v1/projects/{project_id}/peers/{peer_id}/decisions
PUT    /api/v1/projects/{project_id}/peers/{peer_id}/decisions

File attachments

POST   /api/v1/attachments                       # multipart upload
GET    /api/v1/attachments/{id}                  # 下载(302 redirect 到签名 URL)
DELETE /api/v1/attachments/{id}

WebSocket

Endpoint

WSS  /ws?token=<access_token>&device_id=<device_id>

Message format

All WS messages are JSON and include a type field:

{ "type": "message.new", "data": { /* ... */ } }

Client → server

ping                          // 心跳
subscribe.conversation        // 订阅某个对话
unsubscribe.conversation
typing.start
typing.stop
read.ack                      // 已读确认

Server → client

pong
message.new                   // 新消息
message.updated
message.deleted
typing.update                 // 谁在打字
presence.update               // 联系人上下线
permission.request            // 需要用户决定的权限请求
agent.status                  // 我的 Agent 在做什么("正在咨询 ABC Agent...")
conversation.updated

message.new example:

{
  "type": "message.new",
  "data": {
    "id": "01HXKQ...",
    "conversation_id": "01HX...",
    "sender_type": "peer_agent",
    "sender_id": "01HY...",
    "sender_did": "did:web:acme.com:agents:support",
    "content_type": "text",
    "content": "用 0x03 Read Holding Registers...",
    "citations": [
      {
        "source": "X100 通信手册 v3.2",
        "page": 87,
        "url": "https://acme.com/manuals/x100-v3.2.pdf#page=87",
        "trust_level": "authoritative"
      }
    ],
    "language": "zh",
    "created_at": "2024-11-15T14:30:00Z"
  }
}

permission.request example:

{
  "type": "permission.request",
  "data": {
    "id": "01HXP...",
    "level": "L2",
    "action": "share_files",
    "scope": {
      "peer": "did:web:acme.com:agents:support",
      "paths": ["src/modbus/"],
      "exclude": [".env", "secrets/"]
    },
    "description": "Agent 想分享 src/modbus/ 给 ABC Agent(12 个文件)",
    "requested_at": "2024-11-15T14:30:00Z"
  }
}

SSE (LLM streaming)

GET  /api/v1/conversations/{id}/messages/{msg_id}/stream
Accept: text/event-stream

Event types:

event: token
data: {"text":"用 "}

event: token
data: {"text":"0x03 "}

event: tool_call
data: {"tool":"agent_network.ask_peer","args":{...}}

event: tool_result
data: {"result":"..."}

event: citation
data: {"source":"X100 通信手册 v3.2","page":87}

event: done
data: {"finish_reason":"stop","tokens_used":523}

A2A API (outward-facing, called by other Confer instances)

See docs/03-protocol.md for details. This section lists endpoints only.

POST   /a2a/v1/messages                  # 接收外部 Agent 消息
GET    /a2a/v1/stream/{message_id}       # 流式拉回答(SSE)
POST   /a2a/v1/threads                   # 开启对话 thread
GET    /a2a/v1/agent-facts/{agent_did}   # 公开 AgentFacts

All A2A endpoints require HTTP Message Signature verification.

.well-known endpoints

GET    /.well-known/did.json                # 主 DID document
GET    /.well-known/agents.json             # 本实例所有公开 Agent 列表
GET    /.well-known/openid-configuration    # 未来:OIDC 兼容(v2)

Webhooks (optional, v1.5+)

Let external systems subscribe to events:

POST   /api/v1/webhooks
GET    /api/v1/webhooks
DELETE /api/v1/webhooks/{id}

Supported events: message.new.peer, permission.granted, thread.archived.

Rate-limiting policy

Route Limit
/api/v1/auth/login 10/minute per IP
/api/v1/auth/register 3/hour per IP
/api/v1/conversations/*/messages POST 60/minute per user
/a2a/v1/* 100/minute per peer-domain (higher for the allowlist)
WSS up to 10 concurrent connections per user

Rate-limit response:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{ "error": { "code": "rate_limited", "message": "Too many requests" } }

Consultation API (user-initiated outbound A2A)

Lets a user (or an MCP server acting on the user’s behalf) proactively send a question to a peer agent who is already a contact and retrieve the asynchronous reply. Signing and delivery happen entirely inside the gateway; the private key never leaves the gateway.

Difference from the “Conversation API”: /api/v1/conversations + /api/v1/stream is a conversation with your own local LLM assistant; /api/v1/consult is what sends a message via A2A to someone else’s agent.

POST /api/v1/consult/:peerId

Starts or continues a type='consult' conversation (one conversation is reused per peer), signs, and delivers a message.type='question'.

// 请求体(consultRequestSchema)
{ "question": "如何轮换密钥?", "code_context": "...可选代码...", "language": "zh" }
Response Meaning
201 { conversation_id, message_id, status: "sent" } Signed and delivered
502 { ..., status: "failed", error } Delivery failed (peer offline / no endpoint / signature-verification problem)
403 not_a_contact The peer is not a contact of the current user

GET /api/v1/consult/:conversationId/reply?after=:messageId&wait=:seconds

Long-polls for the peer’s asynchronous reply (the peer returns it via inbound /a2a/v1/messages carrying a thread_id, and the gateway reattaches it to this thread by thread_id). The wait cap is 55s.

GET /api/v1/consult/:conversationId

Returns the full message history of the consultation thread (up to 200 messages).

Contract: inbound A2A only triggers a local agent auto-reply for message.type==='question'; answer/notification are merely persisted + broadcast, to avoid consultation replies triggering an infinite back-and-forth.

← Back to Confer A2A · DID:web · RFC 9421 · NANDA