[ Switch to styled version → ]


← Docs index

Webhooks

The daemon can send real-time HTTP POST notifications for events. This document describes the configuration, event types, and payload format for webhooks.

Overview

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.

Configuration

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

Event types

Daemon & node lifecycle events:

Connection events:

Tunnel events:

Trust & handshake events:

Data events:

Pub/Sub events:

Security events:

Policy & Managed events:

Payload format

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

Example receiver

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

Runtime hot-swap

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.