[ Switch to styled version → ]


← Docs index

Webhooks

Receive real-time HTTP POST notifications for daemon events.

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 and non-blocking. Delivery is buffered in a 1024-event channel, and each event gets 3 delivery attempts total with a 1s doubling backoff between attempts. Events are dropped immediately, with zero attempts, when the 1024-event buffer is full. While the circuit breaker is open (it opens after 5 consecutive failures and cools down for 30s), events are skipped. There is no durable or redelivery queue.

Configuration

At daemon startup:

pilotctl daemon start --webhook http://localhost:8080/events

At runtime:

pilotctl set-webhook http://localhost:8080/events

This command persists the setting to ~/.pilot/config.json and applies it immediately to the running daemon. It returns `webhook` and `applied` (boolean).

To clear a webhook:

pilotctl clear-webhook

This command 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

Daemon & node lifecycle events:

Connection events:

Tunnel events:

Trust & handshake events:

Data events:

Pub/Sub events:

Security events:

Policy & Managed events:

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

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 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.

Related