Docs
API & data reference
The external REST API, outgoing webhook payload formats, and exactly what a config export/import does and doesn't carry. Written for the person scripting against Islandr, not for clicking through the Admin Console.
External automation API
REST API
A separate, versioned facade under /api/external/v1/ — deliberately not the same
endpoints the Admin Console's own UI calls. The internal API is free to reshape itself as the UI
evolves; this one is a stability-committed contract for scripts, CI pipelines, and infrastructure-as-code.
Authentication
API keys, not session cookies. Create one in the Admin Console under API Keys — the raw key is shown exactly once at creation, never again. Send it as a bearer token:
curl -H "Authorization: Bearer islandr_<your-key>" \ https://your-islandr-host/api/external/v1/peers
A key acts with the same privileges as the admin who created it — v1 has no per-key scoping yet. Revoking a key is immediate and permanent (revoked keys are kept, not deleted, for the audit trail).
Endpoints (v1)
| Method | Path | Notes |
|---|---|---|
GET | /peers | Every peer, newest first. |
POST | /peers | Create a client peer (pass userId as a query
parameter) or a site gateway peer (omit userId, set "type": "site" in the body).
The response includes the generated private key material exactly once. |
GET | /users | Every user. |
GET | /sites | Every site. |
GET | /resources | Every resource, across all sites. |
GET | /roles | Every role. |
Deliberately small on day one — read-mostly access to what automation actually needs, not a mirror of every internal endpoint. Each additional facade endpoint is an incremental, reviewable addition to the same pattern.
OpenAPI spec
The full machine-readable spec (hand-written, not runtime-generated — it never drifts from an annotation processor's idea of the API, and adds nothing to the native-image build) is served directly by any running instance, no authentication required:
curl https://your-islandr-host/api/openapi.yml
Feed it to openapi-generator, Postman, or any OpenAPI 3.0.3-compatible client generator.
It's also linked from the API Keys page in the Admin Console.
Turning the facade off
An admin who never intends to use external automation can disable the whole facade — every
/api/external/v1/* route then returns 404 (not 401/403,
so it doesn't even confirm the surface exists) regardless of an otherwise-valid key. Toggle in
Settings → External automation API.
Outgoing notifications
Webhooks
Configured per-URL in the Admin Console under Webhooks, each with its own filter of which event types it receives. Delivery: up to 3 attempts with backoff (2s, 8s) — a receiver that's down for a minute still gets the event once it's back. Two payload formats, chosen per webhook.
Event types
| Key | Fired when… |
|---|---|
peer.connected | A peer establishes a handshake after being offline. |
peer.disconnected | A previously connected peer goes stale/silent. |
peer.enabled | A peer is enabled — by an admin or the Peer-Scheduler. |
peer.disabled | A peer is disabled — by an admin or the Peer-Scheduler. |
acl.grant_created | A resource access grant is created. |
acl.grant_revoked | A resource access grant is revoked. |
discovery.scan_completed | A network discovery scan finishes. |
acme.cert_renewed | The ACME/Let's Encrypt certificate renews successfully. |
acme.cert_renewal_failed | An ACME renewal attempt fails. |
Format: generic (default)
Islandr's own JSON envelope, HMAC-SHA256-signed so the receiver can verify authenticity:
{
"event": "peer.connected",
"timestamp": "2026-08-23T07:12:04.331Z",
"actor": "admin",
"target": "Peer:office-laptop",
"data": { "peerId": "3f1c…" }
}
Sent with two headers:
| Header | Value |
|---|---|
X-Islandr-Signature | sha256=<hex hmac> — HMAC-SHA256 of
the raw request body, keyed with the webhook's own secret (shown once at creation, rotatable
afterwards). |
X-Islandr-Delivery | A random UUID, unique per delivery attempt — use it to deduplicate retries on the receiving end. |
HMAC-SHA256(secret, raw_body) yourself and comparing
hex strings — constant-time compare, don't just == it.
Format: Gotify
Renders Gotify's native push shape
instead, and posts straight to its /message endpoint — no separate receiver needed on
your side if you already run a Gotify server:
{
"title": "islandr — Peer connected",
"message": "**Peer:office-laptop**\n- peerId: `3f1c…`",
"priority": 5,
"extras": { "client::display": { "contentType": "text/markdown" } }
}
Priority is 8 (not 5) for acme.cert_renewal_failed and peer.disconnected —
high enough to break through Do-Not-Disturb if the Gotify client is configured that way. No
X-Islandr-Signature header is sent for this format — Gotify has no signature-verification
story, and its own app token (configured as the webhook's "secret") already authenticates the request
via a ?token= query parameter.
Extra auth header (either format)
If the receiving end — or a reverse proxy in front of it — needs its own auth header
(Authorization, X-API-Key, anything), configure one optional name/value
pair per webhook. It's sent on every delivery alongside whatever the format above already sends,
never as a replacement for it.
Backup & migration
Config export/import
A versioned JSON snapshot of the instance's configuration — for backups and for moving an install to new hardware. Downloaded/uploaded from the Admin Console's Settings page. New fields are added over time; an older export missing a field imports with a sensible default rather than failing.
The envelope carries a version (the format), an appVersion (which Islandr
wrote the file, informational only) and exportedAt. Older files import into newer
Islandr; a file written by a newer Islandr is refused rather than imported, because
importing it would silently drop the fields that build doesn't know about and restore a
configuration quietly different from the one backed up. The refusal happens before anything is
touched, so a rejected import leaves the running configuration intact.
Included
- All global settings (WireGuard subnet/MTU/keepalive, tunnel mode, DNS resolver config, hub map
location, feature toggles like
selfServicePeerCreationand the external API's own enable/disable switch, …) — except the handful called out below as host-specific. - Users, roles, role memberships.
- Peers — including their Peer-Scheduler state (
validUntil,enabledSource, and any recurring weekly enable/disable window). - Sites, resources, resource ports, port groups and their members — including a port's exclusive-capacity configuration (how many people may hold it at once, the ceiling on a single reservation, and whether requests are auto-approved).
- Every permanent ACL grant — role-based, type-based, and direct user/site grants — plus their per-port links.
- The two fixed OIDC providers' configuration (Microsoft 365 and Google Workspace: client ID, client secret, tenant/allowed domains).
- Custom OIDC providers added via the generic OIDC flow (Auth0, Okta, Keycloak, or any other issuer) — including discovered endpoints, so a restored instance doesn't need network access to the IdP just to log in again immediately.
- External-API keys. Only the SHA-256 hash and non-secret prefix ever exist — same as at rest — but that's still enough: an already-issued key keeps authenticating after a restore, since verification only ever needs the hash, never the raw value. Revoked keys stay revoked.
- Private key material, only when the export is explicitly requested with
includePrivateKeys=true— off by default.
Not included — and why
- Ad-hoc temporary direct grants (an access grant created with an expiry). These are meant to expire on their own; restoring a backup should never resurrect one that may already be stale by the time the backup is applied. Only permanent direct grants travel with an export.
- Live port reservations. Who currently holds an exclusive port, and until when. Same reasoning as the temporary grants above: a reservation is a running session with an end time, and a restore should start with nobody holding anything rather than reinstating somebody's half-finished slot from whenever the backup was taken. The ports' capacity configuration does travel; only the claims on them don't.
- Webhooks (URLs, secrets, event filters) — not yet part of the export at all.
- Sessions. Ephemeral by nature; nothing to restore.
- TLS/ACME certificate state (certificate, private key, ACME account, pending challenges) — tied to this specific host's domain and network position, not portable to another machine.
- Google Workspace directory-import credentials (the service account JSON) — a host-specific secret, re-entered per install rather than carried in a backup file.
- The hub's own WireGuard identity (server public key, public endpoint) — deliberately kept as the target host's own values on import, not overwritten by the source host's identity.