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.

https://web.peerlify.com/api/v1

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:

curlbash
curl https://web.peerlify.com/api/v1/devices \
  -H "Authorization: Bearer papi_your_token_here"

Base URL

Replace the host if you run a private deployment. On the managed service the base URL is 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.

http
Authorization: Bearer papi_l0Xq…

Create and manage tokens from the admin console:

  1. Open Settings → Keys in the web console, for the network you want the token to act on.
  2. Under API access tokens, choose New token.
  3. Pick a scope (or full access), an optional expiry, and copy the token immediately.

The token carries its network

A token is bound to the network it was created in. You never pass a network ID — the token resolves it server-side. To act on a different network, mint a token there.

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:writescope
List devices, then authorize, rename, tag, set key-expiry, and remove them.
keys:read · keys:writescope
Read the token owner’s auth keys, mint new ones, and revoke them.
network:read · network:writescope
Read the network object; change its name, description, subnet, and approval settings. A separate resource from policy because a subnet change re-addresses every device in the network.
policy:read · policy:writescope
Read network settings and the ACL document, replace the policy, and manage tags.
users:read · users:writescope
List members, change roles, approve, and remove.
webhooks:read · webhooks:writescope
List and manage webhook endpoints.

A 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).

json
{ "error": "You don't have permission for the required scope: devices:write." }
400bad request
Malformed body, an out-of-range page or pageSize, an invalid scope, or a policy that fails its ACL tests.
401unauthorized
Missing, malformed, expired, or revoked token.
402payment required
The network’s plan doesn’t include the requested capability (e.g. an advanced role).
403forbidden
The token’s scopes don’t cover the call, or the action is console-only.
404not found
The addressed device, key, user, tag, or webhook doesn’t exist in this network.
409conflict
The resource already exists (e.g. a tag of that name).
410gone
The token’s network has been deleted.

Pagination

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:

pageinteger
1-based index of the page to return. Defaults to 1.
pageSizeinteger
Rows per page. Defaults to 50, 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:

GET /devices?page=2&pageSize=50json
{
  "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-Countheader
Rows in the whole collection.
X-Page-Countheader
Number of pages at the current pageSize.
X-Page · X-Page-Sizeheader
The window that produced this response.
Linkheader
RFC 8288 relations — next, 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.

page.mjsjavascript
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. A null timestamp 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 items wrapper.
  • IDs in paths are the resource’s public ID — a device’s id, a tag’s bare name, a webhook’s id. Users are addressed by their membership id.
  • Secrets (tokens, auth keys, webhook signing secrets) are returned once, in the response that creates them, and never again.
  • Content type is application/json for 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.

Request bodyjson
{
  "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-Signatureheader
t=<unix>,v1=<hex> — the timestamp and HMAC-SHA256 signature (see below).
Peerlify-Eventheader
The event type, e.g. device.approved.
Peerlify-Deliveryheader
A unique delivery ID, stable across retries of the same delivery.

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

verify.mjsjavascript
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.createddevice
A new device joined the network.
device.approveddevice
A pending device was approved.
device.deleteddevice
A device was removed from the network.
device.key_expiringdevice
A device’s session key is within 24 hours of expiry.
device.key_expireddevice
A device’s session key has expired.
user.approveduser
A member awaiting approval was approved.
user.role_changeduser
A member’s role changed.
user.removeduser
A member was removed from the network.
policy.updatedpolicy
The ACL policy was replaced with a new version.
network.updatednetwork
The network’s settings changed through the API; the payload lists the changed fields and whether device addresses were reassigned.

Looking for install and CLI guides instead? Browse the documentation, start with the install guide or the CLI reference, or download the client.