# Agent Registry (/aiway/a2a/registry)



The **agent registry** is a REST API where agents announce themselves to KubeMQ by
`agent_id` and HTTP URL. It is the source of truth for who can be reached over the A2A
gateway: registering an agent spawns its [virtual subscriber](/aiway/a2a/architecture),
and a TTL-based liveness check removes agents that stop sending heartbeats.

## Overview [#overview]

Every agent that callers can reach through `POST /a2a/{agent_id}` must first be
registered. A registration is an **agent card** — `agent_id`, human-readable `name`,
absolute `url`, and an optional list of skills. The registry persists cards in SQLite,
tracks each agent's `last_seen` time, and replicates state across a cluster so any node
can route to any agent.

The registry exposes five operations as plain HTTP+JSON (not JSON-RPC):

| Operation  | Method · Path                                            | Purpose                                        |
| ---------- | -------------------------------------------------------- | ---------------------------------------------- |
| Register   | `POST /agents/register`                                  | Add or re-register an agent card               |
| List       | `GET /agents`                                            | List agents, optionally filtered by skill tags |
| Get one    | `GET /agents/{agent_id}`                                 | Fetch a single agent's full card               |
| Heartbeat  | `POST /agents/heartbeat`                                 | Refresh `last_seen` to stay alive              |
| Deregister | `POST /agents/deregister` or `DELETE /agents/{agent_id}` | Remove an agent                                |

## How it works [#how-it-works]

The registry is a service backed by SQLite. Registering spawns a virtual subscriber and
emits a replication event; a background liveness checker sweeps expired agents.

<Mermaid
  chart="`
graph LR
CLIENT[&#x22;Registry client&#x22;]
API[&#x22;A2A registry API<br/>:9090/agents/*&#x22;]
SVC[&#x22;Registry service&#x22;]
STORE[&#x22;SQLite store&#x22;]
SUBMGR[&#x22;Subscriber Manager&#x22;]
LIVE[&#x22;Liveness checker<br/>every 60s&#x22;]

CLIENT -- &#x22;register / heartbeat<br/>list / deregister&#x22; --> API
API --> SVC
SVC --> STORE
SVC -- &#x22;spawn / remove&#x22; --> SUBMGR
LIVE -. &#x22;expire stale agents&#x22; .-> STORE

class CLIENT client
class API,SVC aiway
class STORE,SUBMGR,LIVE broker
`"
/>

*Registration persists the card and spawns a virtual subscriber; the liveness checker prunes agents past their TTL.*

## The agent card [#the-agent-card]

An agent card describes one agent. `agent_id`, `name`, and `url` are required; the
`url` must be an absolute `http://` or `https://` address. Server-managed fields
(`registered_at`, `last_seen`) are populated on the response.

| Field                | Type      | Required   | Description                                                          |
| -------------------- | --------- | ---------- | -------------------------------------------------------------------- |
| `agent_id`           | string    | yes        | Unique identifier; 2–128 chars, lowercase alphanumeric and hyphens   |
| `name`               | string    | yes        | Human-readable name (max 256 chars)                                  |
| `url`                | string    | yes        | Absolute `http(s)://` endpoint the gateway POSTs to (max 2048 chars) |
| `description`        | string    | no         | Free-text description (max 2048 chars)                               |
| `version`            | string    | no         | Agent version (max 64 chars)                                         |
| `skills`             | array     | no         | List of `AgentSkill` objects (see below)                             |
| `defaultInputModes`  | string\[] | no         | Default input modes, e.g. `["text"]`                                 |
| `defaultOutputModes` | string\[] | no         | Default output modes, e.g. `["text"]`                                |
| `protocolVersions`   | string\[] | no         | Supported protocol versions; defaults to `["1.0"]`                   |
| `registered_at`      | string    | server-set | Original registration time (preserved across re-registration)        |
| `last_seen`          | string    | server-set | Last heartbeat or registration time                                  |

Each entry in `skills` is an **AgentSkill**: `id` (required), `name` (required),
`description`, and `tags` (used by the [list](#list-agents) filter and skill-based
discovery).

```json
{
  "agent_id": "echo-agent-01",
  "name": "Echo Agent",
  "description": "A simple echo agent for testing",
  "version": "1.0.0",
  "url": "http://localhost:18080/",
  "skills": [
    {
      "id": "echo",
      "name": "Echo",
      "description": "Echoes back the received message",
      "tags": ["test", "echo"]
    }
  ],
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "protocolVersions": ["1.0"]
}
```

## Register an agent [#register-an-agent]

`POST /agents/register` with the agent card as the JSON body. Re-registering the same
`agent_id` upserts the card and &#x2A;*preserves the original `registered_at`**. The response
is the stored card with `registered_at` and `last_seen` populated.

When auth is enabled, the `registered_by` field is set from the JWT principal and is
used for [ownership](#ownership) checks. Registration fails with `400` (validation),
`403` (ownership conflict), or `409` (the `MaxAgents` limit was reached).

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    curl -X POST http://localhost:9090/agents/register \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "echo-agent-01",
        "name": "Echo Agent",
        "description": "A simple echo agent for testing",
        "version": "1.0.0",
        "url": "http://localhost:18080/",
        "skills": [
          {"id": "echo", "name": "Echo", "description": "Echoes back the received message", "tags": ["test", "echo"]}
        ],
        "defaultInputModes": ["text"],
        "defaultOutputModes": ["text"],
        "protocolVersions": ["1.0"]
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    const int AgentPort = 18080;

    var card = new JsonObject
    {
        ["agent_id"] = "echo-agent-01",
        ["name"] = "Echo Agent",
        ["description"] = "A simple echo agent for testing",
        ["version"] = "1.0.0",
        ["url"] = $"http://localhost:{AgentPort}/",
        ["skills"] = new JsonArray(new JsonObject
        {
            ["id"] = "echo", ["name"] = "Echo",
            ["description"] = "Echoes back the received message",
            ["tags"] = new JsonArray("test", "echo")
        }),
        ["defaultInputModes"] = new JsonArray("text"),
        ["defaultOutputModes"] = new JsonArray("text"),
        ["protocolVersions"] = new JsonArray("1.0")
    };

    using var client = new HttpClient();
    var resp = await client.PostAsync(
        $"{KubeMqUrl}/agents/register",
        new StringContent(card.ToJsonString(), Encoding.UTF8, "application/json"));
    Console.WriteLine($"Registered: {(int)resp.StatusCode}");
    var body = await resp.Content.ReadAsStringAsync();
    Console.WriteLine(JsonSerializer.Serialize(JsonNode.Parse(body), new JsonSerializerOptions { WriteIndented = true }));
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )

    const (
    	kubemqURL = "http://localhost:9090"
    	agentID   = "echo-agent-01"
    	agentPort = 18080
    )

    func main() {
    	card := map[string]interface{}{
    		"agent_id":    agentID,
    		"name":        "Echo Agent",
    		"description": "A simple echo agent for testing",
    		"version":     "1.0.0",
    		"url":         fmt.Sprintf("http://localhost:%d/", agentPort),
    		"skills": []map[string]interface{}{
    			{
    				"id":          "echo",
    				"name":        "Echo",
    				"description": "Echoes back the received message",
    				"tags":        []string{"test", "echo"},
    			},
    		},
    		"defaultInputModes":  []string{"text"},
    		"defaultOutputModes": []string{"text"},
    		"protocolVersions":   []string{"1.0"},
    	}
    	data, _ := json.Marshal(card)
    	resp, err := http.Post(kubemqURL+"/agents/register", "application/json", bytes.NewReader(data))
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Registration failed: %v\n", err)
    		os.Exit(1)
    	}
    	defer resp.Body.Close()
    	body, _ := io.ReadAll(resp.Body)
    	fmt.Printf("Registered: %d\n", resp.StatusCode)
    	var pretty bytes.Buffer
    	json.Indent(&pretty, body, "", "  ")
    	fmt.Println(pretty.String())
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.List;
    import java.util.Map;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final int AGENT_PORT = 18080;
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var card = Map.of(
                "agent_id", "echo-agent-01",
                "name", "Echo Agent",
                "description", "A simple echo agent for testing",
                "version", "1.0.0",
                "url", "http://localhost:" + AGENT_PORT + "/",
                "skills", List.of(Map.of(
                    "id", "echo", "name", "Echo",
                    "description", "Echoes back the received message",
                    "tags", List.of("test", "echo"))),
                "defaultInputModes", List.of("text"),
                "defaultOutputModes", List.of("text"),
                "protocolVersions", List.of("1.0")
            );

            var client = HttpClient.newHttpClient();
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents/register"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(card)))
                .build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
            System.out.println("Registered: " + resp.statusCode());
            var data = MAPPER.readTree(resp.body());
            System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data));
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio
    import json

    import httpx

    KUBEMQ_URL = "http://localhost:9090"
    AGENT_PORT = 18080


    async def main() -> None:
        card = {
            "agent_id": "echo-agent-01",
            "name": "Echo Agent",
            "description": "A simple echo agent for testing",
            "version": "1.0.0",
            "url": f"http://localhost:{AGENT_PORT}/",
            "skills": [
                {
                    "id": "echo",
                    "name": "Echo",
                    "description": "Echoes back the received message",
                    "tags": ["test", "echo"],
                }
            ],
            "defaultInputModes": ["text"],
            "defaultOutputModes": ["text"],
            "protocolVersions": ["1.0"],
        }
        async with httpx.AsyncClient() as client:
            resp = await client.post(f"{KUBEMQ_URL}/agents/register", json=card)
            print(f"Registered: {resp.status_code}")
            print(json.dumps(resp.json(), indent=2))


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";
    const AGENT_PORT = 18080;

    async function main() {
      const card = {
        agent_id: "echo-agent-01",
        name: "Echo Agent",
        description: "A simple echo agent for testing",
        version: "1.0.0",
        url: `http://localhost:${AGENT_PORT}/`,
        skills: [
          {
            id: "echo",
            name: "Echo",
            description: "Echoes back the received message",
            tags: ["test", "echo"],
          },
        ],
        defaultInputModes: ["text"],
        defaultOutputModes: ["text"],
        protocolVersions: ["1.0"],
      };

      const resp = await fetch(`${KUBEMQ_URL}/agents/register`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(card),
      });

      const data = await resp.json();
      console.log("Registered:", JSON.stringify(data, null, 2));
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## List agents [#list-agents]

`GET /agents` returns a bare JSON array `[<AgentCard>, ...]`. Add `?skill_tags=tag1,tag2`
(comma-separated) to filter by skill tags, and `?limit=N` to cap the page size. Skill-tag
filtering is applied in memory after fetching, enabling skill-based discovery.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # All agents
    curl http://localhost:9090/agents

    # Filter by skill tags
    curl "http://localhost:9090/agents?skill_tags=echo"
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    using var client = new HttpClient();

    Console.WriteLine("=== All Agents ===");
    var resp = await client.GetAsync($"{KubeMqUrl}/agents");
    var agentsRoot = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
    var agents = agentsRoot is JsonArray arr ? arr : agentsRoot["agents"]!.AsArray();
    foreach (var agent in agents)
    {
        var skills = agent!["skills"]?.AsArray().Select(s => s!["id"]!.GetValue<string>()).ToList() ?? [];
        Console.WriteLine($"  {agent["agent_id"]}: skills=[{string.Join(", ", skills)}]");
    }
    Console.WriteLine($"\nTotal agents: {agents.Count}");

    Console.WriteLine("\n=== Filter by skill_tags=echo ===");
    resp = await client.GetAsync($"{KubeMqUrl}/agents?skill_tags=echo");
    var echoRoot = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
    var filtered = echoRoot is JsonArray echoArr ? echoArr : echoRoot["agents"]!.AsArray();
    foreach (var agent in filtered)
        Console.WriteLine($"  {agent!["agent_id"]}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )

    const kubemqURL = "http://localhost:9090"

    func listAgents(url string, label string) {
    	resp, err := http.Get(url)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
    		return
    	}
    	defer resp.Body.Close()
    	body, _ := io.ReadAll(resp.Body)

    	// GET /agents returns a bare JSON array: [<AgentCard>, ...]
    	var agents []map[string]interface{}
    	if err := json.Unmarshal(body, &agents); err != nil {
    		// Fallback for a {"agents":[...]} wrapper, if ever present.
    		var wrapper map[string]interface{}
    		json.Unmarshal(body, &wrapper)
    		if raw, ok := wrapper["agents"].([]interface{}); ok {
    			for _, a := range raw {
    				if m, ok := a.(map[string]interface{}); ok {
    					agents = append(agents, m)
    				}
    			}
    		}
    	}

    	fmt.Printf("=== %s ===\n", label)
    	for _, agent := range agents {
    		fmt.Printf("  %s\n", agent["agent_id"])
    	}
    	fmt.Printf("\nTotal: %d\n\n", len(agents))
    }

    func main() {
    	listAgents(kubemqURL+"/agents", "All Agents")
    	listAgents(kubemqURL+"/agents?skill_tags=echo", "Filter by skill_tags=echo")
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var client = HttpClient.newHttpClient();

            System.out.println("=== All Agents ===");
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents"))
                .GET().build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
            var root = MAPPER.readTree(resp.body());
            var agents = root.isArray() ? root : root.get("agents");
            for (var agent : agents) {
                System.out.println("  " + agent.get("agent_id").asText());
            }
            System.out.println("\nTotal agents: " + agents.size());
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            print("=== All Agents ===")
            resp = await client.get(f"{KUBEMQ_URL}/agents")
            data = resp.json()
            agents = data.get("agents", data) if isinstance(data, dict) else data
            for agent in agents:
                skills = [s["id"] for s in agent.get("skills", [])]
                print(f"  {agent['agent_id']}: skills={skills}")
            print(f"\nTotal agents: {len(agents)}")

            print("\n=== Filter by skill_tags=echo ===")
            resp = await client.get(f"{KUBEMQ_URL}/agents", params={"skill_tags": "echo"})
            data = resp.json()
            filtered = data.get("agents", data) if isinstance(data, dict) else data
            for agent in filtered:
                print(f"  {agent['agent_id']}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";

    async function main() {
      console.log("=== List all agents ===");
      const allResp = await fetch(`${KUBEMQ_URL}/agents`);
      const allData = await allResp.json();
      const allAgents = Array.isArray(allData) ? allData : (allData.agents || []);
      console.log(`Found ${allAgents.length} agent(s):`);
      for (const agent of allAgents) {
        const skillIds = (agent.skills || []).map((s: { id?: string }) => s.id).filter(Boolean);
        console.log(`  - ${agent.agent_id} (skills: ${skillIds.join(", ") || "none"})`);
      }

      console.log("\n=== Filter by skill_tags=echo ===");
      const echoResp = await fetch(`${KUBEMQ_URL}/agents?skill_tags=echo`);
      const echoData = await echoResp.json();
      const echoAgents = Array.isArray(echoData) ? echoData : (echoData.agents || []);
      for (const agent of echoAgents) {
        console.log(`  - ${agent.agent_id}`);
      }
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## Get one agent [#get-one-agent]

`GET /agents/{agent_id}` returns the full agent card, or `404` if the agent is not
registered. Use it to inspect server-managed fields like `registered_at` and `last_seen`.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    curl http://localhost:9090/agents/echo-agent-01
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text.Json;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    const string AgentId = "echo-agent-01";

    using var client = new HttpClient();
    var resp = await client.GetAsync($"{KubeMqUrl}/agents/{AgentId}");
    Console.WriteLine($"Status: {(int)resp.StatusCode}");
    var data = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
    Console.WriteLine($"  agent_id:      {data["agent_id"]}");
    Console.WriteLine($"  name:          {data["name"]}");
    Console.WriteLine($"  url:           {data["url"]}");
    Console.WriteLine($"  registered_at: {data["registered_at"]}");
    Console.WriteLine($"  last_seen:     {data["last_seen"]}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )

    const (
    	kubemqURL = "http://localhost:9090"
    	agentID   = "echo-agent-01"
    )

    func main() {
    	resp, err := http.Get(kubemqURL + "/agents/" + agentID)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
    		os.Exit(1)
    	}
    	defer resp.Body.Close()

    	body, _ := io.ReadAll(resp.Body)
    	fmt.Printf("Status: %d\n", resp.StatusCode)

    	var data map[string]interface{}
    	json.Unmarshal(body, &data)
    	fmt.Printf("  agent_id:      %v\n", data["agent_id"])
    	fmt.Printf("  name:          %v\n", data["name"])
    	fmt.Printf("  url:           %v\n", data["url"])
    	fmt.Printf("  registered_at: %v\n", data["registered_at"])
    	fmt.Printf("  last_seen:     %v\n", data["last_seen"])
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final String AGENT_ID = "echo-agent-01";
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var client = HttpClient.newHttpClient();
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents/" + AGENT_ID))
                .GET().build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
            System.out.println("Status: " + resp.statusCode());

            var data = MAPPER.readTree(resp.body());
            System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data));
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"
    AGENT_ID = "echo-agent-01"


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            resp = await client.get(f"{KUBEMQ_URL}/agents/{AGENT_ID}")
            print(f"Status: {resp.status_code}")
            data = resp.json()
            print(f"  agent_id:      {data.get('agent_id')}")
            print(f"  name:          {data.get('name')}")
            print(f"  url:           {data.get('url')}")
            print(f"  registered_at: {data.get('registered_at')}")
            print(f"  last_seen:     {data.get('last_seen')}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";
    const AGENT_ID = "echo-agent-01";

    async function main() {
      const resp = await fetch(`${KUBEMQ_URL}/agents/${AGENT_ID}`);
      const agent = await resp.json();
      console.log(`  agent_id:      ${agent.agent_id}`);
      console.log(`  name:          ${agent.name}`);
      console.log(`  url:           ${agent.url}`);
      console.log(`  registered_at: ${agent.registered_at}`);
      console.log(`  last_seen:     ${agent.last_seen}`);
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## Heartbeat [#heartbeat]

`POST /agents/heartbeat` with `{"agent_id": "..."}` refreshes the agent's `last_seen`
time. An agent must heartbeat (or re-register) within its [TTL](#ttl-and-liveness) to
avoid being expired. When auth is enabled, the ownership check applies. The response is
`{"ok": true}`; heartbeating an unregistered agent returns an error.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    curl -X POST http://localhost:9090/agents/heartbeat \
      -H "Content-Type: application/json" \
      -d '{"agent_id": "echo-agent-01"}'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    const string AgentId = "echo-agent-01";

    using var client = new HttpClient();
    var body = new JsonObject { ["agent_id"] = AgentId };
    var resp = await client.PostAsync(
        $"{KubeMqUrl}/agents/heartbeat",
        new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
    var data = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
    Console.WriteLine($"Heartbeat: status={(int)resp.StatusCode} last_seen={data["last_seen"]}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )

    const (
    	kubemqURL = "http://localhost:9090"
    	agentID   = "echo-agent-01"
    )

    func main() {
    	body, _ := json.Marshal(map[string]string{"agent_id": agentID})
    	resp, err := http.Post(kubemqURL+"/agents/heartbeat", "application/json", bytes.NewReader(body))
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Heartbeat failed: %v\n", err)
    		os.Exit(1)
    	}
    	defer resp.Body.Close()
    	raw, _ := io.ReadAll(resp.Body)

    	var data map[string]interface{}
    	json.Unmarshal(raw, &data)
    	fmt.Printf("Heartbeat: status=%d last_seen=%v\n", resp.StatusCode, data["last_seen"])
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.Map;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final String AGENT_ID = "echo-agent-01";
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var client = HttpClient.newHttpClient();
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents/heartbeat"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                    MAPPER.writeValueAsString(Map.of("agent_id", AGENT_ID))))
                .build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
            var data = MAPPER.readTree(resp.body());
            System.out.println("Heartbeat: status=" + resp.statusCode()
                + " last_seen=" + data.path("last_seen").asText());
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"
    AGENT_ID = "echo-agent-01"


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"{KUBEMQ_URL}/agents/heartbeat",
                json={"agent_id": AGENT_ID},
            )
            data = resp.json()
            print(f"Heartbeat: status={resp.status_code} last_seen={data.get('last_seen')}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";
    const AGENT_ID = "echo-agent-01";

    async function main() {
      const resp = await fetch(`${KUBEMQ_URL}/agents/heartbeat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ agent_id: AGENT_ID }),
      });
      const data = await resp.json();
      console.log(`Heartbeat: status=${resp.status}, last_seen=${data.last_seen}`);
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## Deregister [#deregister]

Remove an agent with either `POST /agents/deregister` (body `{"agent_id": "..."}`) or
`DELETE /agents/{agent_id}`. Deregistering deletes the card, stops the agent's virtual
subscriber, and drains in-flight requests. Both methods return `{"ok": true}`; when auth
is enabled, the [ownership](#ownership) check applies.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # Deregister via POST
    curl -X POST http://localhost:9090/agents/deregister \
      -H "Content-Type: application/json" \
      -d '{"agent_id": "echo-agent-01"}'

    # Or via DELETE
    curl -X DELETE http://localhost:9090/agents/echo-agent-01
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    const string AgentId = "echo-agent-01";

    using var client = new HttpClient();

    // Deregister via POST
    var body = new JsonObject { ["agent_id"] = AgentId };
    var postResp = await client.PostAsync(
        $"{KubeMqUrl}/agents/deregister",
        new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json"));
    Console.WriteLine($"POST deregister: {(int)postResp.StatusCode}");

    // Or via DELETE
    var delResp = await client.DeleteAsync($"{KubeMqUrl}/agents/{AgentId}");
    Console.WriteLine($"DELETE: {(int)delResp.StatusCode}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"net/http"
    	"os"
    )

    const (
    	kubemqURL = "http://localhost:9090"
    	agentID   = "echo-agent-01"
    )

    func main() {
    	// Deregister via POST
    	body, _ := json.Marshal(map[string]string{"agent_id": agentID})
    	resp, err := http.Post(kubemqURL+"/agents/deregister", "application/json", bytes.NewReader(body))
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Deregister POST failed: %v\n", err)
    		os.Exit(1)
    	}
    	resp.Body.Close()
    	fmt.Printf("POST /agents/deregister: %d\n", resp.StatusCode)

    	// Or via DELETE
    	req, _ := http.NewRequest(http.MethodDelete, kubemqURL+"/agents/"+agentID, nil)
    	resp, err = http.DefaultClient.Do(req)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "DELETE failed: %v\n", err)
    		os.Exit(1)
    	}
    	resp.Body.Close()
    	fmt.Printf("DELETE /agents/%s: %d\n", agentID, resp.StatusCode)
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final String AGENT_ID = "echo-agent-01";

        public static void main(String[] args) throws Exception {
            var client = HttpClient.newHttpClient();

            // Deregister via POST
            var postReq = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents/deregister"))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(
                    "{\"agent_id\": \"" + AGENT_ID + "\"}"))
                .build();
            var postResp = client.send(postReq, HttpResponse.BodyHandlers.ofString());
            System.out.println("POST deregister: " + postResp.statusCode());

            // Or via DELETE
            var delReq = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents/" + AGENT_ID))
                .DELETE()
                .build();
            var delResp = client.send(delReq, HttpResponse.BodyHandlers.ofString());
            System.out.println("DELETE: " + delResp.statusCode());
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"
    AGENT_ID = "echo-agent-01"


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            # Deregister via POST
            resp = await client.post(
                f"{KUBEMQ_URL}/agents/deregister",
                json={"agent_id": AGENT_ID},
            )
            print(f"POST /agents/deregister: {resp.status_code}")

            # Or via DELETE
            resp = await client.delete(f"{KUBEMQ_URL}/agents/{AGENT_ID}")
            print(f"DELETE /agents/{AGENT_ID}: {resp.status_code}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";
    const AGENT_ID = "echo-agent-01";

    async function main() {
      // Deregister via POST
      const postResp = await fetch(`${KUBEMQ_URL}/agents/deregister`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ agent_id: AGENT_ID }),
      });
      console.log(`POST deregister status: ${postResp.status}`);

      // Or via DELETE
      const delResp = await fetch(`${KUBEMQ_URL}/agents/${AGENT_ID}`, {
        method: "DELETE",
      });
      console.log(`DELETE status: ${delResp.status}`);
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## TTL and liveness [#ttl-and-liveness]

The registry expires agents that go silent. A background **liveness checker** runs every
60 seconds and deletes any agent whose `last_seen` is older than `AgentTTLSeconds`
(default `300` — five minutes). Expiring an agent also removes its virtual subscriber and
emits a `deregister` replication event.

To stay registered, an agent must [heartbeat](#heartbeat) (or re-register) within the TTL
window. A safe interval is well under `AgentTTLSeconds` — for the default 300s TTL, a
heartbeat every 60–120 seconds gives plenty of margin.

<Callout type="info">
  Tune the window with `AgentTTLSeconds`. See
  [Configuration](/aiway/a2a/configuration) for the full `A2aConfig` field set
  and the disable env var.
</Callout>

## Ownership [#ownership]

When authentication is enabled, the registry records the JWT principal that registered
each agent in `registered_by`. Heartbeat and deregister then enforce an **ownership
check**: only the registering principal may refresh or remove the agent. Cross-principal
re-registration is rejected with `403` (ownership conflict).

The check **fails closed**: if auth is enabled and an agent's `registered_by` is blank,
no principal can modify or delete it. See
[Authentication](/aiway/a2a/guides/authentication) for the auth model and
`X-KubeMQ-Caller-ID` propagation.

## MaxAgents limit [#maxagents-limit]

`MaxAgents` caps the total number of registered agents. The default is `0`, meaning
**unlimited**. When a positive limit is set and reached, new registrations are rejected
with `409` (conflict) — re-registering an existing agent still succeeds, since it does not
grow the count.

## Response and status codes [#response-and-status-codes]

| Status | Operation                         | Meaning                                         |
| ------ | --------------------------------- | ----------------------------------------------- |
| `200`  | register / heartbeat / get / list | Success                                         |
| `400`  | register / heartbeat              | Validation error (bad card, missing `agent_id`) |
| `403`  | register / heartbeat / deregister | Ownership conflict (auth enabled)               |
| `404`  | get / deregister                  | Agent not found                                 |
| `409`  | register                          | `MaxAgents` limit reached                       |

## Related [#related]

<Cards>
  <Card title="Agent cards" href="/aiway/a2a/agent-cards" description="The well-known agent-card endpoints and the platform vs individual card." />

  <Card title="How it works" href="/aiway/a2a/architecture" description="Virtual subscribers, internal channels, and registry replication across a cluster." />

  <Card title="Configuration" href="/aiway/a2a/configuration" description="A2aConfig fields including AgentTTLSeconds, MaxAgents, and the disable env var." />

  <Card title="Reference" href="/aiway/a2a/reference" description="Endpoint, JSON-RPC, AgentCard schema, and metrics reference tables." />
</Cards>
