Webhooks

Receive a signed HTTPS POST when devices, members, the access policy or network settings change. Events, payloads, signatures and retries.

Updated · ·By Ivan Vyshniak

Nothing on this page changes with your selection.

A webhook sends an HTTPS POST to your endpoint when something changes in a network. Webhooks are available on every plan.

Create an endpoint

Requires the Owner, Admin or Network admin role.

  1. Open Settings → Webhooks and click Add endpoint.
  2. Endpoint: enter the Payload URL and, optionally, a Label.
  3. Events: choose the events to send.
  4. Delivery: leave Active on, or turn it off to pause deliveries without deleting the endpoint.
  5. Click Create endpoint, then copy the signing secret (whsec_…) shown under Save the signing secret. It isn't shown again. If you lose it, see Replace the secret.

The URL must:

  • use https;
  • resolve only to public addresses. localhost, private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), 100.64.0.0/10 (which includes Peerlify network addresses), link-local, loopback and multicast addresses are refused. Addresses and localhost are checked when you save; host names are checked at every delivery. The form's hint suggests http://localhost works for testing; it doesn't.

Endpoints can also be managed with the REST API.

Events

EventSent when
device.createdA device joins the network by any route. Detected by a check that runs every 60 seconds, so it can arrive up to a minute late.
device.approvedA pending device is approved in the console or through the API. Not sent for devices approved automatically when they join.
device.deletedA device is removed from the network.
device.key_expiringA device's session expires within 24 hours.
device.key_expiredA device's session has just expired.
user.approvedA pending member is approved.
user.role_changedA member's role changes.
user.removedA member is removed.
policy.updatedThe access policy is saved or restored, including exit-point rule changes.
network.updatedNetwork settings are changed through the REST API. Changes made on the Settings page don't send it.

Payload

device.approvedjson
{
  "type": "device.approved",
  "timestamp": "2026-10-09T14:03:12.4810000Z",
  "networkId": "k3x9q2m7pa",
  "data": {
    "id": "481516234",
    "name": "build-server",
    "hostname": "build-server",
    "addresses": ["100.100.0.12"],
    "tags": ["tag:server"],
    "status": "approved",
    "online": true,
    "lastSeen": "2026-10-09T14:03:05.1200000Z",
    "os": "linux",
    "osVersion": "Ubuntu 24.04.1 LTS",
    "keyExpiryDisabled": false,
    "ephemeral": false,
    "created": "2026-10-09T13:58:41.9030000Z",
    "networkId": "k3x9q2m7pa"
  }
}
  • timestamp is when the event happened.
  • device.approved and device.deleted carry a device object like the one above. device.created carries a shorter one: id, name, hostname, addresses, tags, status. device.key_expiring carries { "device": {…}, "expiresAt": "…" } and device.key_expired carries { "device": {…}, "expiredAt": "…" }, with the same short device object. Fields without a value are left out.
  • User events carry { "user": { id, userId, name, email, role, approved, joined } }, and user.role_changed adds from and to.
  • policy.updated carries version, ruleCount and exitPointRuleCount.

Every request also has these headers:

HeaderValue
Peerlify-EventThe event type.
Peerlify-DeliveryAn ID for this delivery to this endpoint, the same on every retry.
Peerlify-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
User-AgentPeerlify-Webhooks/1.0

Verify the signature

The signature is an HMAC-SHA256 of <t>.<raw request body>, keyed with the whole signing secret, including the whsec_ prefix. t is the time of this delivery attempt, so it changes on retries.

  1. Read t and v1 from Peerlify-Signature.
  2. Compute the HMAC over t, a ., and the exact bytes of the body.
  3. Compare it with v1 in constant time.
  4. Reject requests whose t is far from your clock.
verify.pypython
import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig) and abs(time.time() - int(t)) <= tolerance

Delivery and retries

  • A delivery succeeds when your endpoint returns any 2xx within 10 seconds. Redirects count as failures.
  • Failed deliveries are retried after 1, 2, 4, 8 and 16 minutes: six attempts in all. Pending retries survive restarts.
  • Deliveries aren't guaranteed to arrive in order, and the same event can arrive more than once. Make your handler idempotent; Peerlify-Delivery identifies retries of one delivery.
  • Pausing or deleting an endpoint fails its pending retries. They aren't resumed when you turn it back on.
  • Endpoints aren't disabled automatically after repeated failures.

Test and monitor

Each endpoint's row shows its state: Healthy, Last delivery failed or Paused. The icons on the row are:

  • Send test event sends {"type": "test", …, "data": {"message": "This is a test event from Peerlify."}} regardless of the subscribed events. Limited to 10 per minute.
  • Delivery log shows the last 25 deliveries with their HTTP status.
  • Edit and Delete.

Replace the secret

The console has no button for this. Either delete the endpoint and create it again, or call POST /api/v1/webhooks/{id}/rotate with an API token that has the webhooks:write scope. The response contains the new secret, and the old one stops working immediately.

Next steps

Was this page helpful?