Webhooks

Receive real-time HTTP POST notifications for daemon events.

On this page

Overview

When configured, the daemon POSTs a JSON event to your webhook URL every time something happens — connections, trust changes, messages received, pub/sub activity, and more. Events are delivered asynchronously and non-blocking; if the endpoint is down, events are dropped (no queuing).

Configuration

At daemon startup

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

At runtime

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

Persists to ~/.pilot/config.json and applies immediately to the running daemon.

Returns: webhook, applied (bool — true if daemon is running and accepted the change)

Clear webhook

pilotctl clear-webhook

Removes the webhook URL from config and the running daemon. Returns: webhook, applied (bool)

Via config file

You can also set the webhook URL 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

EventDescription
node.registeredDaemon registered with the registry
node.reregisteredRe-registration after keepalive timeout
node.deregisteredDaemon deregistered from the registry

Connections

EventDescription
conn.syn_receivedIncoming connection request
conn.establishedConnection fully established
conn.finConnection closed gracefully (FIN)
conn.rstConnection reset
conn.idle_timeoutConnection timed out due to inactivity

Tunnels

EventDescription
tunnel.peer_addedNew tunnel peer discovered
tunnel.establishedTunnel handshake completed
tunnel.relay_activatedRelay fallback activated for a peer (symmetric NAT)

Trust & handshakes

EventDescription
handshake.receivedTrust handshake request received from a peer
handshake.pendingHandshake queued for approval
handshake.approvedHandshake approved (by you)
handshake.rejectedHandshake rejected (by you)
handshake.auto_approvedMutual handshake — auto-approved
trust.revokedTrust revoked locally (you untrusted a peer)
trust.revoked_by_peerTrust revoked by a remote peer

Data

EventDescription
message.receivedTyped message received via data exchange (port 1001)
file.receivedFile received via data exchange (port 1001)
data.datagramDatagram received

Pub/Sub

EventDescription
pubsub.subscribedSubscriber joined a topic
pubsub.unsubscribedSubscriber left a topic
pubsub.publishedEvent published to a topic

Security

EventDescription
security.syn_rate_limitedSYN rate limiter triggered
security.nonce_replayNonce replay detected (potential attack)

Payload format

Every webhook POST contains a JSON body with this structure:

{
  "event": "handshake.received",
  "node_id": 5,
  "timestamp": "2026-01-15T12:34:56.789Z",
  "data": {
    "peer_node_id": 7,
    "justification": "want to collaborate"
  }
}
FieldTypeDescription
eventstringThe event type (e.g. conn.established)
node_iduint32Your node's ID (the daemon emitting the event)
timestampstringISO 8601 timestamp
dataobjectEvent-specific data (may be null for some events)

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

You can change the webhook URL while the daemon is running. The new URL takes effect immediately — no restart needed:

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