[ Switch to styled version → ]


← Docs index

Webhooks

The daemon sends real-time HTTP POST notifications for events. Events are delivered asynchronously to a configured webhook URL.

Overview

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.

Configuration

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"
}

Event types

Node lifecycle events:

Connection events:

Tunnel events:

Trust & handshake events:

Data events:

Pub/Sub events:

Security events:

Policy & Managed events:

Payload format

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:

Example receiver

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

Runtime hot-swap

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.