[ Switch to styled version → ]
The daemon sends real-time HTTP POST notifications for events. Events are delivered asynchronously to a configured webhook URL.
When configured, the daemon POSTs a JSON event to a webhook URL for events such as connections, trust changes, messages received, and pub/sub activity. Events are delivered asynchronously. If the endpoint is unavailable, events are dropped.
Set the webhook at daemon startup:
pilotctl daemon start --webhook http://localhost:8080/events Set the webhook 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 'webhook' and 'applied' (boolean).
Clear the webhook:
pilotctl clear-webhook This removes the webhook URL from the configuration and the running daemon. It returns 'webhook' and 'applied' (boolean).
The webhook URL can also be set in ~/.pilot/config.json:
{
"registry": "34.71.57.205:9000",
"beacon": "34.71.57.205:9001",
"webhook": "http://localhost:8080/events"
} Node lifecycle events:
Connection events:
Tunnel events:
Trust & handshake events:
Data events:
Pub/Sub events:
Security events:
Policy & Managed events:
Every webhook POST contains a JSON body with this 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"
}
} Payload fields:
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. The new URL takes effect immediately.
# 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, so it survives daemon restarts.