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.
- Open Settings → Webhooks and click Add endpoint.
- Endpoint: enter the Payload URL and, optionally, a Label.
- Events: choose the events to send.
- Delivery: leave Active on, or turn it off to pause deliveries without deleting the endpoint.
- 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 andlocalhostare checked when you save; host names are checked at every delivery. The form's hint suggestshttp://localhostworks for testing; it doesn't.
Endpoints can also be managed with the REST API.
Events
| Event | Sent when |
|---|---|
device.created | A 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.approved | A pending device is approved in the console or through the API. Not sent for devices approved automatically when they join. |
device.deleted | A device is removed from the network. |
device.key_expiring | A device's session expires within 24 hours. |
device.key_expired | A device's session has just expired. |
user.approved | A pending member is approved. |
user.role_changed | A member's role changes. |
user.removed | A member is removed. |
policy.updated | The access policy is saved or restored, including exit-point rule changes. |
network.updated | Network settings are changed through the REST API. Changes made on the Settings page don't send it. |
Payload
{
"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"
}
}timestampis when the event happened.device.approvedanddevice.deletedcarry a device object like the one above.device.createdcarries a shorter one:id,name,hostname,addresses,tags,status.device.key_expiringcarries{ "device": {…}, "expiresAt": "…" }anddevice.key_expiredcarries{ "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 } }, anduser.role_changedaddsfromandto. policy.updatedcarriesversion,ruleCountandexitPointRuleCount.
Every request also has these headers:
| Header | Value |
|---|---|
Peerlify-Event | The event type. |
Peerlify-Delivery | An ID for this delivery to this endpoint, the same on every retry. |
Peerlify-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
User-Agent | Peerlify-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.
- Read
tandv1fromPeerlify-Signature. - Compute the HMAC over
t, a., and the exact bytes of the body. - Compare it with
v1in constant time. - Reject requests whose
tis far from your clock.
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)) <= toleranceDelivery and retries
- A delivery succeeds when your endpoint returns any
2xxwithin 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-Deliveryidentifies 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.