[ Switch to styled version → ]
The daemon can send real-time HTTP POST notifications for events. This document describes the configuration, event types, and payload format for webhooks.
When a webhook URL is configured, the daemon sends a JSON event via HTTP POST for events such as connections, trust changes, messages, and pub/sub activity. Events are delivered asynchronously. If the endpoint is unavailable, events are dropped without being queued.
A webhook can be configured at daemon startup.
pilotctl daemon start --webhook http://localhost:8080/events A webhook can be set at runtime.
pilotctl set-webhook http://localhost:8080/events This command persists the URL to ~/.pilot/config.json and applies it to the running daemon. It returns the webhook URL and an 'applied' boolean indicating if the running daemon accepted the change.
To clear a webhook:
pilotctl clear-webhook This removes the webhook URL from the configuration and the running daemon. It returns the cleared webhook URL and an 'applied' boolean.
The webhook URL can also be set directly in ~/.pilot/config.json.
{
"registry": "34.71.57.205:9000",
"beacon": "34.71.57.205:9001",
"webhook": "http://localhost:8080/events"
} Daemon & node lifecycle events:
Connection events:
Tunnel events:
Trust & handshake events:
Data events:
Pub/Sub events:
Security events:
Policy & Managed events:
Each webhook POST contains a JSON body with the following structure.
{
"event_id": 1,
"event": "handshake.received",
"node_id": 5,
"timestamp": "2026-01-15T12:34:56.789Z",
"data": {
"peer_node_id": 7,
"justification": "want to collaborate"
}
} This is a minimal webhook receiver in Python.
#!/usr/bin/env python3
# webhook_receiver.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
event = body["event"]
data = body.get("data", {})
if event == "handshake.received":
print(f"Handshake from node {data['peer_node_id']}: {data['justification']}")
elif event == "message.received":
print(f"Message from {data['from']}: {data['type']}")
elif event == "file.received":
print(f"File received: {data['filename']} ({data['size']} bytes)")
else:
print(f"Event: {event}")
self.send_response(200)
self.end_headers()
def log_message(self, *args):
pass # suppress request logs
HTTPServer(("", 8080), Handler).serve_forever() # Start the receiver, then configure the webhook:
python3 webhook_receiver.py &
pilotctl set-webhook http://localhost:8080/events The webhook URL can be changed while the daemon is running, and the new URL takes effect immediately without a restart.
# Switch to a new endpoint
pilotctl set-webhook http://localhost:9090/v2/events
# Disable webhooks temporarily
pilotctl clear-webhook
# Re-enable
pilotctl set-webhook http://localhost:8080/events The webhook URL is persisted to ~/.pilot/config.json and survives daemon restarts.