Developers
API reference
A JSON REST API over your network’s devices, auth keys, access policy, members, and webhooks. Everything the admin console can do to a network, a token can do too.
Overview
The API is served from the web console host under /api/v1. Every request and response is JSON, every mutation is audit-logged, and every token is scoped to a single network — the same network you were viewing in the console when you minted it.
Get started in one request. Create a token (see Authentication), then call any read endpoint:
curl https://web.peerlify.com/api/v1/devices \ -H "Authorization: Bearer papi_your_token_here"
Base URL
https://web.peerlify.com/api/v1.Authentication
Authenticate with a bearer token in the Authorization header. Tokens begin with the prefix papi_ and are shown to you exactly once, at creation — only a hash is stored, so a lost token can’t be recovered, only revoked and replaced.
Authorization: Bearer papi_l0Xq…
Create and manage tokens from the admin console:
- Open Settings → Keys in the web console, for the network you want the token to act on.
- Under API access tokens, choose New token.
- Pick a scope (or full access), an optional expiry, and copy the token immediately.
The token carries its network
Scopes
Each token holds a set of scopes shaped as resource:access. A token with no scopes has full access within its network; add scopes to narrow it. A :write scope always implies its :read counterpart, so devices:write alone is enough to both list and modify devices.
devices:read · devices:writescopekeys:read · keys:writescopenetwork:read · network:writescopepolicy because a subnet change re-addresses every device in the network.policy:read · policy:writescopeusers:read · users:writescopewebhooks:read · webhooks:writescopeA call that exceeds the token’s scopes returns 403 with a message naming the scope it needed.
Errors
Errors use standard HTTP status codes and a JSON body with a single error field (some validation errors add a details array).
{ "error": "You don't have permission for the required scope: devices:write." }400bad requestpage or pageSize, an invalid scope, or a policy that fails its ACL tests.401unauthorized402payment required403forbidden404not found409conflict410gonePagination
Every endpoint that returns a collection is paginated. Networks grow, and an unbounded list response eventually becomes a timeout for whoever has the largest one. The window is two query parameters:
pageinteger1.pageSizeinteger50, and the maximum is 200.The response keeps the collection under its own name — devices, keys, users, webhooks, tags — and adds a pagination object describing the whole collection, not the page you were handed:
{
"devices": [ /* 50 devices */ ],
"pagination": {
"page": 2,
"pageSize": 50,
"totalCount": 137,
"totalPages": 3,
"hasNextPage": true,
"hasPreviousPage": true
}
}The same counts come back as headers, for clients that would rather read those:
X-Total-CountheaderX-Page-CountheaderpageSize.X-Page · X-Page-SizeheaderLinkheadernext, prev, first, last — as absolute URLs. Absent when everything fits on one page.Paging through a collection
Stop when hasNextPage is false. Asking for a page past the last one is not an error — you get an empty array and honest counts, so a loop that overshoots terminates instead of throwing.
const base = "https://web.peerlify.com/api/v1/devices";
const headers = { Authorization: `Bearer ${process.env.PEERLIFY_TOKEN}` };
let page = 1;
const devices = [];
while (true) {
const res = await fetch(`${base}?page=${page}&pageSize=200`, { headers });
if (!res.ok) throw new Error((await res.json()).error);
const body = await res.json();
devices.push(...body.devices);
if (!body.pagination.hasNextPage) break;
page++;
}
console.log(`${devices.length} devices`);An out-of-range window is a 400
page below 1, or a pageSize outside 1–200, is rejected with 400 rather than quietly clamped — a request for pageSize=1000 that silently returned 200 rows would look exactly like a complete answer.Results are ordered newest-first by creation time, with a stable tiebreaker, so a row can’t appear on two pages while another goes missing. Rows created or deleted while you page will still shift the window — if you need an exact snapshot of a large network, page from newest to oldest and reconcile by id.
Conventions
- Timestamps are ISO 8601 in UTC, e.g.
2026-07-20T18:30:00.0000000Z. Anulltimestamp means the event hasn’t happened (never used, no expiry). - Collections are paginated — see Pagination. The collection always keeps its own field name; there is no generic
itemswrapper. - IDs in paths are the resource’s public ID — a device’s
id, a tag’s bare name, a webhook’sid. Users are addressed by their membershipid. - Secrets (tokens, auth keys, webhook signing secrets) are returned once, in the response that creates them, and never again.
- Content type is
application/jsonfor every request body.
30 endpoints
Devices
Every machine enrolled in the network. A device here is its membership in this network — the per-network name, address, tags, and approval state.
Auth keys
Pre-issued keys that enroll devices unattended, the headless equivalent of signing in through a browser. A key belongs to the token's network and only ever enrolls devices into it; it acts as the user who created it.
Network & policy
The network object, its access-control document, and the tag definitions the rules refer to.
Users
Members of the network and the roles that scope what they can do.
Webhooks
HTTPS endpoints that receive a signed POST whenever a subscribed event fires.
Delivery & signatures
Each delivery is a JSON POST to your URL with a common envelope: the event type, an ISO timestamp, the networkId, and an event-specific data object.
{
"type": "device.approved",
"timestamp": "2026-07-20T18:30:00.0000000Z",
"networkId": "k3m9qz1w7p",
"data": {
"id": "482913756",
"name": "ci-runner-07"
}
}Every request carries these headers:
Peerlify-Signatureheadert=<unix>,v1=<hex> — the timestamp and HMAC-SHA256 signature (see below).Peerlify-Eventheaderdevice.approved.Peerlify-DeliveryheaderVerifying the signature
The signature is HMAC-SHA256 over the string “<timestamp>.<raw-request-body>”, keyed with the endpoint’s secret. Recompute it over the raw body you received and compare in constant time. Reject deliveries whose timestamp is far from your clock to defeat replays.
import crypto from 'node:crypto';
// secret: the whsec_… value from the create response
// header: the Peerlify-Signature header value
// rawBody: the exact request body bytes, before JSON parsing
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(
header.split(',').map(kv => kv.split('=')),
);
const signed = `${parts.t}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signed)
.digest('hex');
const a = Buffer.from(parts.v1);
const b = Buffer.from(expected);
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}Retries
A delivery succeeds on any 2xx response within 10 seconds. Otherwise it retries with exponential backoff — 1, 2, 4, 8, then 16 minutes — for up to 6 attempts, after which it is marked failed. Retries are persisted, so they survive a server restart. Make your handler idempotent: use the Peerlify-Delivery ID to deduplicate.
Event catalog
The events a webhook can subscribe to:
device.createddevicedevice.approveddevicedevice.deleteddevicedevice.key_expiringdevicedevice.key_expireddeviceuser.approveduseruser.role_changeduseruser.removeduserpolicy.updatedpolicynetwork.updatednetworkLooking for install and CLI guides instead? Browse the documentation, start with the install guide or the CLI reference, or download the client.