Version: 0.1.0 | Last updated: 2026-03-06
All hashes are HoloHash base64 strings (e.g. uhCAk..., uhCEk..., uhCkk...).
All responses are JSON. Errors return { "error": "<message>", "code": <http_status> }.
Authentication is optional. When H2HC_LINKER_ADMIN_SECRET is set, all DHT/K2 endpoints require a session token via Authorization: Bearer <token>. When unset, all endpoints are open.
| Capability | Protects |
|---|---|
dht_read |
GET /dht/* endpoints |
dht_write |
POST /dht/*/publish |
k2 |
GET /k2/* endpoints |
Sessions are scoped to registered cells (agent+DNA pairs). When a client sends a Register message on the WebSocket for a given DNA, that DNA is added to the session's authorized set. HTTP requests to /dht/{dna_hash}/* endpoints are checked against this set — requests for unregistered DNAs return 403 Forbidden.
Sessions have no TTL. They live until:
- The WebSocket connection disconnects (all sessions for that agent are revoked)
- The agent is removed via
DELETE /admin/agents(all sessions and connections are closed) - A session is explicitly revoked
- Admin registers an agent via
POST /admin/agents(requiresAuthorization: Bearer <admin_secret>) - Agent connects via WebSocket, completes challenge-response auth (ed25519 signature)
- Agent receives a session token (no expiry)
- Agent sends
Registermessages for each DNA it needs to access - Agent uses
Authorization: Bearer <session_token>for HTTP requests (scoped to registered DNAs)
| Code | Meaning |
|---|---|
401 Unauthorized |
Missing, invalid, or revoked session token |
403 Forbidden |
Valid token but insufficient capability or unregistered DNA |
No auth required.
Response 200:
{
"status": "ok",
"version": "0.1.0"
}All DHT endpoints query the network directly via kitsune2 wire protocol.
Get a record by action hash or entry hash.
| Parameter | In | Type | Description |
|---|---|---|---|
dna_hash |
path | string | DNA hash (base64) |
hash |
path | string | Action hash or entry hash (base64) |
Auth: dht_read (when auth enabled)
Response 200: A flat record object, or null if not found.
{
"signature": "<base64>",
"action": {
"type": "Create",
"author": "uhCAk...",
"timestamp": [1234567890, 0],
"action_seq": 5,
"prev_action": "uhCkk...",
"entry_type": { ... },
"entry_hash": "uhCEk...",
"weight": { ... }
},
"entry": {
"entry_type": "App",
"entry": "<base64 msgpack>"
}
}Get details for a hash, including updates and deletes (matches Holochain's get_details return format).
| Parameter | In | Type | Description |
|---|---|---|---|
dna_hash |
path | string | DNA hash (base64) |
hash |
path | string | Action hash or entry hash (base64) |
Auth: dht_read
Response 200: Returns either Details::Record or Details::Entry depending on the hash type, or null if not found.
For an action hash (record details):
{
"type": "Record",
"content": {
"record": { ... },
"validation_status": "Valid",
"updates": [ ... ],
"deletes": [ ... ]
}
}For an entry hash (entry details):
{
"type": "Entry",
"content": {
"entry": "<base64 msgpack>",
"entry_type": "App",
"actions": [ ... ],
"updates": [ ... ],
"deletes": [ ... ],
"entry_dht_status": "Live"
}
}Get links from a base hash.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
dna_hash |
path | string | yes | DNA hash (base64) |
base |
query | string | yes | Base hash — agent pubkey, entry hash, action hash, or external hash (base64) |
zome_index |
query | u8 | no | Zome index to filter by. Required when type is provided. |
type |
query | u16 | no | Link type index to filter by. Requires zome_index. |
tag |
query | string | no | Link tag prefix filter (base64 encoded) |
Auth: dht_read
Response 200: Array of link objects.
[
{
"author": "uhCAk...",
"target": "uhCEk...",
"timestamp": [1234567890, 0],
"zome_index": 0,
"link_type": 1,
"tag": "<base64>",
"create_link_hash": "uhCkk..."
}
]Returns [] if no links found.
Errors:
400iftypeprovided withoutzome_index
Count links from a base hash (same query parameters as GET /dht/{dna_hash}/links).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
dna_hash |
path | string | yes | DNA hash (base64) |
base |
query | string | yes | Base hash (base64) |
zome_index |
query | u8 | no | Zome index filter |
type |
query | u16 | no | Link type filter (requires zome_index) |
tag |
query | string | no | Tag prefix filter (base64 encoded) |
Auth: dht_read
Response 200:
{
"count": 42
}Get agent activity (chain status and optionally full action list).
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
dna_hash |
path | string | yes | DNA hash (base64) |
agent_hash |
path | string | yes | Agent pubkey (base64) |
request |
query | string | no | "status" for status only, "full" (default) for full activity |
Auth: dht_read
Response 200: Agent activity response (Holochain AgentActivityResponse format), or null if not found.
Get agent activity filtered by chain position.
| Parameter | In | Type | Description |
|---|---|---|---|
dna_hash |
path | string | DNA hash (base64) |
Request body:
{
"agent": "uhCAk...",
"chain_top": "uhCkk...",
"include_cached_entries": false
}| Field | Type | Required | Description |
|---|---|---|---|
agent |
string | yes | Agent pubkey (base64) |
chain_top |
string | yes | Action hash to start from (base64) |
include_cached_entries |
bool | no | Include cached entries (default: false) |
Auth: dht_read
Response 200: Filtered agent activity, or null if not found.
Publish signed DhtOps to the DHT network.
| Parameter | In | Type | Description |
|---|---|---|---|
dna_hash |
path | string | DNA hash (base64) |
Auth: dht_write
Request body:
{
"ops": [
{
"op_data": "<base64 msgpack-encoded DhtOp>",
"signature": "<base64 64-byte Ed25519 signature>"
}
]
}Response 200:
{
"success": true,
"queued": 3,
"failed": 0,
"published": 3,
"results": [
{ "success": true },
{ "success": true },
{ "success": false, "error": "reason" }
]
}| Field | Type | Description |
|---|---|---|
success |
bool | true if no storage failures AND at least some ops reached peers |
queued |
number | Ops successfully stored in TempOpStore |
failed |
number | Ops that failed to store |
published |
number | Ops published to at least one DHT peer |
results |
array | Per-op result in request order |
Unlike /dht/* endpoints which query the network directly via kitsune2 wire protocol, zome calls are proxied through a Holochain conductor (configured via H2HC_LINKER_CONDUCTOR_URL). Without a conductor running, this endpoint returns 503.
Call a zome function via the conductor.
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
dna_hash |
path | string | yes | DNA hash (base64) |
zome_name |
path | string | yes | Zome name |
fn_name |
path | string | yes | Function name |
payload |
query | string | no | Base64 URL-safe encoded JSON payload |
Auth: None (not protected even when auth enabled)
Response 200: JSON-encoded zome function return value.
Errors:
503if conductor not connected
Overall network status.
Auth: k2
Response 200:
{
"connected": true,
"bootstrap_url": "http://127.0.0.1:4422",
"relay_url": "https://relay.example.com",
"total_peers": 5,
"active_spaces": 2,
"full_arc_peers": 3,
"blocked_peers": 0,
"ready": true
}| Field | Type | Description |
|---|---|---|
connected |
bool | Kitsune2 is enabled and has a running instance |
total_peers |
number | Known peers across all spaces |
active_spaces |
number | Number of spaces (DNAs) joined |
full_arc_peers |
number | Peers with full DHT arcs (conductors) |
blocked_peers |
number | Peers currently blocking our messages |
ready |
bool | Has full-arc peers and none blocking |
List all known peers across all spaces.
Auth: k2
Response 200: Array of peer info objects.
[
{
"agent_id": "<base64>",
"space_id": "<base64>",
"created_at": 1709000000000000,
"expires_at": 1709003600000000,
"is_tombstone": false,
"url": "iroh://...",
"storage_arc": {
"arc_type": "full"
}
}
]storage_arc.arc_type is one of: "empty", "full", or "arc" (with start and length fields).
Status for a specific space (DNA).
| Parameter | In | Type | Description |
|---|---|---|---|
space_id |
path | string | Space ID (base64 URL-safe no padding) |
Auth: k2
Response 200:
{
"space_id": "<base64>",
"local_agents": 2,
"peer_count": 5
}Errors: 404 if space not found.
List peers in a specific space.
Auth: k2
Response 200: Array of peer info objects (same format as /k2/peers).
Errors: 404 if space not found.
List local agents registered in a space.
Auth: k2
Response 200: Array of agent IDs (base64 strings).
["<agent_id_base64>", "<agent_id_base64>"]Errors: 404 if space not found.
Transport-level network statistics (kitsune2 ApiTransportStats).
Auth: k2
Response 200:
{
"transport_stats": {
"backend": "iroh",
"peer_urls": ["iroh://..."],
"connections": [...]
},
"blocked_message_counts": {}
}Only available when H2HC_LINKER_ADMIN_SECRET is set. All admin endpoints require Authorization: Bearer <admin_secret>.
Add or update an allowed agent.
Request body:
{
"agent_pubkey": "uhCAk...",
"capabilities": ["dht_read", "dht_write", "k2"],
"label": "My Browser Agent"
}| Field | Type | Required | Description |
|---|---|---|---|
agent_pubkey |
string | yes | HoloHash base64 agent pubkey |
capabilities |
array | yes | Array of capability strings: "dht_read", "dht_write", "k2" |
label |
string | no | Human-readable label |
Response: 204 No Content
Remove an agent. Revokes all sessions and closes WebSocket connections.
Request body:
{
"agent_pubkey": "uhCAk..."
}Response: 204 No Content or 404 Not Found
List all allowed agents.
Response 200:
{
"agents": [
{
"agent_pubkey": "uhCAk...",
"capabilities": ["dht_read", "dht_write"],
"label": "My Agent"
}
]
}The WebSocket endpoint handles authentication, agent registration, signal delivery, and remote signing. All messages are JSON with a type field discriminator. The connection must authenticate before any other operations.
Client must authenticate before registering agents or sending signals.
When auth is disabled (no H2HC_LINKER_ADMIN_SECRET): sends auth, immediately receives auth_ok with empty token.
When auth is enabled: challenge-response flow using ed25519 signatures.
Client sends:
{ "type": "auth", "agent_pubkey": "uhCAk..." }Server responds with a challenge:
{ "type": "auth_challenge", "challenge": "<hex 32 bytes>" }Client signs the challenge and responds:
{ "type": "auth_challenge_response", "signature": "<base64 64-byte ed25519 sig>" }Server verifies and responds:
{ "type": "auth_ok", "session_token": "<hex 64 chars>" }The session_token is used for HTTP Authorization: Bearer <token> on subsequent requests. The session has no TTL — it lives until the WebSocket disconnects. After auth, the client must send Register messages for each DNA it wants to access; HTTP requests are scoped to registered DNAs.
On failure:
{ "type": "auth_error", "message": "reason" }Register an agent for a DNA to receive signals and join the kitsune2 space.
Client sends:
{ "type": "register", "dna_hash": "uhC0k...", "agent_pubkey": "uhCAk..." }Server confirms:
{ "type": "registered", "dna_hash": "uhC0k...", "agent_pubkey": "uhCAk..." }To unregister:
{ "type": "unregister", "dna_hash": "uhC0k...", "agent_pubkey": "uhCAk..." }Server confirms:
{ "type": "unregistered", "dna_hash": "uhC0k...", "agent_pubkey": "uhCAk..." }Receiving signals (from another agent via kitsune2):
{
"type": "signal",
"dna_hash": "uhC0k...",
"to_agent": "uhCAk...",
"from_agent": "uhCAk...",
"zome_name": "profiles",
"signal": "<base64 msgpack payload>"
}Sending signals (fire-and-forget, no response):
{
"type": "send_remote_signal",
"dna_hash": "uhC0k...",
"signals": [
{
"target_agent": [<byte array>],
"zome_call_params": [<byte array>],
"signature": [<64 byte array>]
}
]
}When kitsune2 needs to publish agent info, the server requests a signature from the browser. The agent info fields are sent in structured form so the browser can validate what it's signing.
Server sends:
{
"type": "sign_agent_info",
"request_id": "<id>",
"agent_pubkey": "uhCAk...",
"agent_info": {
"agent": "<base64>",
"space": "<base64>",
"createdAt": "1731690797907204",
"expiresAt": "1731762797907204",
"isTombstone": false,
"url": "iroh://...",
"storageArc": [0, 4294967295]
}
}Client signs and responds:
{
"type": "sign_response",
"request_id": "<id>",
"signature": "<base64 64-byte ed25519 sig>"
}Or on failure:
{
"type": "sign_response",
"request_id": "<id>",
"signature": null,
"error": "reason"
}Client sends:
{ "type": "ping" }Server responds:
{ "type": "pong" }The server also sends WebSocket-level pings. Connections are dropped after heartbeat timeout (default 40s with no response) or idle timeout (default 5 minutes).
Server can send errors at any point:
{ "type": "error", "message": "description" }Inject a test signal (development only, no auth).
| Variable | Required | Description |
|---|---|---|
H2HC_LINKER_BOOTSTRAP_URL |
yes | Kitsune2 bootstrap server URL |
H2HC_LINKER_RELAY_URL |
no | Iroh relay server URL |
H2HC_LINKER_CONDUCTOR_URL |
no | Conductor address for zome call proxying |
H2HC_LINKER_ADMIN_SECRET |
no | Enables auth layer when set |
H2HC_LINKER_PAYLOAD_LIMIT_BYTES |
no | Max request payload size (default: 10MB) |
H2HC_LINKER_ZOME_CALL_TIMEOUT_MS |
no | Zome call timeout (default: 10000ms) |
H2HC_LINKER_REPORT |
no | Reporting mode: "json_lines" or "none" |
H2HC_LINKER_REPORT_PATH |
no | Directory for report files (default: /tmp/h2hc-linker-reports) |
H2HC_LINKER_REPORT_DAYS_RETAINED |
no | Report file retention in days (default: 5) |
H2HC_LINKER_REPORT_INTERVAL_S |
no | Fetched-op report interval in seconds (default: 60) |
All errors follow the same format:
{
"error": "descriptive message",
"code": 400
}| HTTP Status | Condition |
|---|---|
| 400 | Malformed request, invalid hash, bad parameters |
| 401 | Missing or invalid authentication |
| 403 | Authenticated but insufficient capabilities |
| 404 | Resource not found (space, agent) |
| 500 | Internal server error |
| 502 | Conductor/network error |
| 503 | Conductor not connected (for zome call endpoint) |