How to receive and verify PeerBand webhooks in your own app
Add a webhook with the Standard Webhooks format. Each request has webhook-id, webhook-timestamp and webhook-signature headers. Verify the HMAC-SHA256 of id.timestamp.body with your whsec_ secret, against the raw body, before you parse it.
Add the endpoint
- In the dashboard, open Webhooks, press + Add webhook and choose Standard Webhooks.
- Enter your HTTPS URL and pick the events.
- Copy the signing secret (
whsec_…). It's shown once. Store it only on the receiving side. - PeerBand sends a signed
webhook.testright away. The webhook turns Active when your endpoint answers 2xx. Until then it shows Pending, and Verify tries again.
What you receive
The body is a JSON envelope with metadata only, plus the note text for agent.notified:
{
"eventId": "0b8e5c1e-4f7a-4d0e-9a51-2f3c6d1e7a90",
"name": "agent.status_changed",
"timestamp": "2026-10-25T10:00:00Z",
"machineId": "5d7a2c4e-9b1f-4e83-a6d2-0c4f8e1b3a77",
"machineName": "office-pc",
"data": {
"agentId": "…",
"status": "blocked",
"previousStatus": "working",
"label": "claude",
"workspace": "api",
"revision": 42
}
}Event names: agent.status_changed, agent.notified, machine.online, machine.offline and webhook.test. The headers:
webhook-id: 0b8e5c1e-4f7a-4d0e-9a51-2f3c6d1e7a90
webhook-timestamp: 1761386400
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=Verify in Node.js
With the standardwebhooks package. Pass the raw body string, not parsed JSON:
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.PEERBAND_WEBHOOK_SECRET); // "whsec_…"
// rawBody: the request body exactly as received (a string, not parsed JSON).
// Throws if the signature or timestamp is wrong.
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"],
"webhook-timestamp": req.headers["webhook-timestamp"],
"webhook-signature": req.headers["webhook-signature"],
});Verify in Python
import base64, hashlib, hmac, time
def verify_peerband(secret: str, headers, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id", "")
timestamp = headers.get("webhook-timestamp", "0")
if not msg_id or abs(time.time() - int(timestamp)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
# During a secret rotation the header holds two signatures.
for entry in headers.get("webhook-signature", "").split():
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature, expected):
return True
return FalseHandle retries properly
- Delivery is at least once.
webhook-idstays the same across retries, so use it to drop duplicates. - Order isn't guaranteed. Compare
timestamp, and the agent'srevisionfor status changes. - Answer 2xx quickly and do the work afterwards. Failed attempts are retried immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours (about 27 hours in all). Requests time out after 10 seconds.
- Answer
410 Goneto disable the webhook at once. - Reject timestamps more than 5 minutes off, to stop replays.
- After you rotate the secret, the old and new secrets both sign for 24 hours, and the header holds both signatures.
Questions
Can I test against localhost?
Not directly. PeerBand only calls public HTTPS addresses, and refuses private, loopback and cloud-metadata addresses. Use a tunnel such as ngrok during development.
Are redirects followed?
No. Point the webhook at the final URL.
Does it work with n8n or Zapier?
Yes. Use their webhook trigger URL with the Standard Webhooks format, and route on the name and data.status fields.