Version 1, 2026-09-05
Read-only REST API for Netrinos accounts on the Edge plan. It exposes the account’s users, devices, virtual devices, and port forwards, plus a short record of devices going online and offline. The intended use is keeping an external system, such as an ERP or asset database, synchronized with the devices registered in Netrinos.
| Item | Value |
|---|---|
| Plan | Edge only |
| Base URL | https://app.netrinos.com/api/v1 |
| Transport | HTTPS, GET only |
| Format | JSON |
Netrinos support issues the account’s API key on request. Once issued, it is visible on the account page in the portal.
GET.
Devices, users, and virtual devices are managed in the Netrinos
portal.The major version is in the path. Fields may be added to a response
and new values may appear in enumerated fields without a version change,
so ignore fields you do not recognize. A breaking change would ship as
/api/v2.
The API key is required on every request. There is no health check to
call without one, and an empty or absent key is never valid. The one
thing you can learn without a key is that a path does not exist: an
unknown path, or a method other than GET, returns 404
before the key is checked.
curl -H "Authorization: Bearer 3f2a1c7e-9b44-4d21-8c6f-0a1e2d3b4c5d" \
https://app.netrinos.com/api/v1/account
One key is active per account, and it grants read access to that account only. It is shown on your account page in the portal, so you can retrieve it whenever you need it. An account with no key set has no API access at all; ask support to issue one.
Ask support to replace the key if it is exposed. A new key takes effect immediately and the old one stops working, so plan the swap.
Treat the key as a credential. Send it only over HTTPS, keep it out of source control, and do not embed it in a browser client.
Field names are camelCase. Timestamps in responses are RFC 3339 in
UTC and may carry fractional seconds, for example
2026-09-04T14:22:31.582Z or
2026-09-04T14:22:31Z. Parse them as RFC 3339 rather than
matching a fixed layout. A timestamp that is unknown or does not apply
is null rather than a zero date. A string field with no
value is "", never null. Array fields are
always present, and empty rather than null.
Lists have a fixed order. Devices are sorted by name,
users by username, and events newest first.
since accepts either form and matches rows strictly
after the instant:
| Form | Example | Meaning |
|---|---|---|
| RFC 3339 | 2026-09-01T00:00:00Z |
After that instant, any offset |
| Duration | 25h, 7d, 90m |
That long before now, on our clock |
A duration is a number followed by m, h, or
d. d is exactly 24 hours, not a calendar day,
and cannot be combined with other units; 1h30m is accepted,
1d12h is not. Durations save a daily job from keeping a
watermark: since=25h covers the last day with an hour of
overlap, every time, with nothing to compute.
List endpoints return the matching rows and a count.
{
"data": [],
"total": 0
}
There is no paging. Responses are capped at 1000 rows, which covers
any normal account in one request. total is the number of
rows that matched, so if it ever exceeds the number returned you have
hit the cap and hold the first 1000 in list order; contact support if
that happens.
Two requests per second sustained per account, with bursts of up to
20. The limit is per account, so every integration using the key shares
it. Beyond that the API returns 429 with a Retry-After
header. A daily sync uses a handful of requests; the limit exists to
stop a runaway loop, not to constrain normal use.
Errors use HTTP status codes and a single body shape.
{
"error": {
"code": "plan_required",
"message": "The Account API requires the Edge plan."
}
}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request |
A query parameter was malformed |
| 401 | unauthorized |
Key missing, unknown, or cleared |
| 403 | account_inactive |
Account suspended, canceled, or deleted |
| 403 | plan_required |
Account is not on the Edge plan |
| 404 | not_found |
No such endpoint, device, or not a GET |
| 429 | rate_limited |
Too many requests, see Retry-After |
| 500 | internal |
Server error, safe to retry |
The API does not send CORS headers. Call it from a server, not a browser.
Confirms the key works and identifies the account it belongs to.
Response:
{
"id": "acmecorp",
"name": "Acme Corporation",
"plan": "edge",
"created": "2024-03-11T09:14:02Z"
}
| Field | Type | Notes |
|---|---|---|
id |
string | Account identifier |
name |
string | Account name |
plan |
string | Always edge on this API |
created |
string | Account creation time |
Users in the account.
Response:
{
"data": [
{
"id": "jsmith.acmecorp",
"username": "jsmith",
"email": "jsmith@acme.example",
"name": "J. Smith",
"role": "owner",
"created": "2024-03-11T09:14:02Z"
}
],
"total": 4
}
| Field | Type | Notes |
|---|---|---|
id |
string | username.account, stable |
username |
string | Login name, unique in the account |
email |
string | Contact address, can change or be empty |
name |
string | Display name |
role |
string | owner, admin, editor, or
user |
created |
string | When the user was added |
Each account has exactly one owner. Use
username to join a device to its user, and id
as a durable key.
Devices registered in the account. Each record embeds its virtual devices and port forwards, so one pass over the list is a complete picture.
Query parameters:
| Parameter | Values | Effect |
|---|---|---|
since |
time, see Conventions | Only devices registered after this |
Response:
{
"data": [
{
"id": "8f14e45f-ceea-467a-9f0a-1c2d3e4f5a6b",
"name": "host0001",
"dnsName": "host0001.acmecorp.2ho.ca",
"type": "edge",
"description": "Site gateway, warehouse",
"platform": "linux arm64",
"version": "1.3.2",
"build": "260905.1121",
"online": true,
"lastSeen": "2026-09-04T14:21:58Z",
"secureIp": "100.96.4.17",
"lanIp": "192.168.20.10",
"isp": "Rogers",
"owner": "jsmith",
"tags": ["warehouse"],
"created": "2025-11-02T16:40:11Z",
"virtualDevices": [
{
"name": "nvr",
"dnsName": "nvr.host0001.acmecorp.2ho.ca",
"description": "Site NVR",
"secureIp": "100.96.4.31",
"lanIp": "192.168.20.40",
"created": "2025-11-04T10:02:44Z"
}
],
"portForwards": [
{
"name": "camera-lobby",
"hostName": "cam-lobby",
"vendor": "Axis",
"ip": "192.168.20.51",
"port": 8080,
"protocol": "tcp",
"service": "http",
"enabled": true,
"updated": "2026-09-01T03:12:00Z"
}
]
}
],
"total": 137
}
| Field | Type | Notes |
|---|---|---|
id |
string | Device GUID, see below |
name |
string | Short DNS label, lowercase |
dnsName |
string | Full name on the Netrinos network |
type |
string | edge or client |
description |
string | Free text set in the portal |
platform |
string | OS and architecture, see below |
version |
string | Netrinos client version |
build |
string | Client build stamp, YYMMDD.HHMM |
online |
boolean | See Device online state |
lastSeen |
string | Last contact, null if never |
secureIp |
string | Address on the Netrinos network |
lanIp |
string | Address on its local network, if reported |
isp |
string | Internet provider short name, see below |
owner |
string | Username of the owning user |
tags |
array | Strings, empty if none |
created |
string | When the device registered |
virtualDevices |
array | See below, empty if none |
portForwards |
array | See below, empty if none |
platform is reported by the device as operating system
and architecture separated by a space, for example
linux arm64, linux amd64,
windows amd64, darwin arm64,
android arm64, or ios arm64. Treat it as free
text; new values can appear as platforms are added.
dnsName is empty for a moment after registration, before
the device is placed in a network. Every other string field that can be
empty says so in its notes.
isp is the short name of the provider behind the
device’s public address at its last sync, for example
Rogers, Bell, or TELUS. It is
looked up from the address, so it identifies the connection the device
is on rather than the account holder’s contract. It is empty when the
device has not synced since registration or the address is not
attributed to a provider.
Use id as the foreign key in your system. It survives
renames, and name and dnsName do not. It also
survives deletion: a device deleted in the portal is logged out, and if
someone logs it in again locally it registers with the same
id. See Detecting additions and removals.
A virtual device is a LAN device published on the Netrinos network under its own address.
| Field | Type | Notes |
|---|---|---|
name |
string | DNS label on the Netrinos network |
dnsName |
string | Full name, name.<device dnsName> |
description |
string | Free text |
secureIp |
string | Address on the Netrinos network |
lanIp |
string | LAN address it maps to, see below |
created |
string | When the virtual device was allocated |
lanIp is reported by the hosting device on each sync
from client 1.3.2 onward. It is empty for a device on an older client or
one that has not synced since the virtual device was added.
Port forwards are reported by EdgeNodes from the LAN they sit on. They are observed data, refreshed on each device sync, and are not present on client devices.
| Field | Type | Notes |
|---|---|---|
name |
string | Rule name as reported |
hostName |
string | Host the rule points at |
vendor |
string | Vendor inferred from the hardware |
ip |
string | LAN address of the target |
port |
number | Forwarded port |
protocol |
string | tcp or udp |
service |
string | Service inferred from the port |
enabled |
boolean | Whether the rule is active |
updated |
string | Last change to the rule, or null |
One device by its id, exactly as returned in the list,
in the same shape as a list entry and not wrapped in an envelope.
Returns 404 if the id is unknown, belongs to another account, or names a device that has been removed. The three cases are not distinguished.
What changed, newest first: devices added and removed, and devices going online and offline. One feed, one shape.
Query parameters:
| Parameter | Values | Effect |
|---|---|---|
since |
time, see Conventions | Only events after this |
type |
comma list, see below | Only these events; default is all |
An unknown type name is a 400, not an empty list. Names
are matched without regard to case.
Response:
{
"data": [
{
"id": "8f14e45f-ceea-467a-9f0a-1c2d3e4f5a6b",
"name": "host0001",
"event": "offline",
"at": "2026-09-04T02:14:09Z"
},
{
"id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
"name": "oldnvr",
"event": "removed",
"at": "2026-09-03T11:07:20Z"
}
],
"total": 2
}
| Field | Type | Notes |
|---|---|---|
id |
string | Device id; for removed, the id it had |
name |
string | Name at the time |
event |
string | added, removed, online, or
offline |
at |
string | When it happened |
| Event | Fires when |
|---|---|
added |
A device logs in and registers |
removed |
A device is deleted in the portal |
online |
A device makes contact after being offline |
offline |
A device has been silent for 15 minutes |
added and removed cover every device.
online and offline cover EdgeNodes unless
tagged noalert in the portal, and client devices only when
tagged alert.
Events are kept for at least 7 days; the purge runs
a few times a day, so rows can linger a few hours past that. They are
for keeping up, not for history; copy what you need.
removed rows carry the identity the device had when it
went, so they still name it after it is gone. An offline
row means the device had been silent for the threshold described under
Device online state, not that it failed at that instant.
The same feed narrowed to one device, in the same shape, with the
same since and type parameters and the same
retention. A device that was deleted and logged in again shows its whole
history under the one id, including the earlier
removed.
Returns 404 for an id that is unknown, belongs to another account, or
has been removed, exactly as GET /devices/{id} does.
Call
GET /devices/events?since=25h&type=added,removed once a
day. Each added row names a device to create; fetch it with
GET /devices/{id} for the full record. Each
removed row names a device to retire. The 25 hours give an
hour of overlap, so nothing is missed if the job runs late, and
re-applying a change you already applied should be a no-op on your
side.
Deleting a device in the portal logs it out. If someone then logs it
in again on the device itself, it registers under the same
id and an added row follows the
removed one. So treat removed as deactivate
rather than delete, treat added for an id you
already hold as reactivate, and when one id has both rows
in a window apply them in at order, oldest first; the feed
itself is newest first.
Once a day, also pull GET /devices in full and compare
it against what you hold, keyed on id. The event feed is
what keeps you current; the full list is what makes you certain. A diff
cannot miss anything, and it is one request.
GET /devices?since=25h is the same idea for additions
only, and returns full device records rather than event rows.
online is true when the device’s Netrinos agent has
reached Netrinos within the last 15 minutes. How it connects does not
matter: a device on a direct path and one going through a relay report
the same way.
Detection is asymmetric by design. Going offline takes at least 15
minutes of silence, because two to five minute outages are routine on
cellular and carrier-grade NAT links and a shorter window would report
working sites as down. Coming back online is immediate on the next
contact. Each response is a snapshot rather than a history, so a device
that failed and recovered between two polls reads as online in both.
GET /devices/events has the transitions a poll cannot show
you, for at least the last 7 days.
A device that comes up may start on a relayed path and settle to a
direct one moments later. That transition is invisible here.
online measures whether the device reaches Netrinos, not
how, so settling changes nothing about the flag and produces no
transition in the history.
online reports that the network path to the device is
up. Netrinos has no visibility above that layer, so it says nothing
about the health of the device or of anything attached to it.
A gateway pegged at 100 percent CPU still answers Netrinos, because the agent needs almost nothing to reply. The device reads as online while the cameras behind it are timing out. A full disk, a stopped service, and a dead camera all look identical from here. The reverse case is just as real: a transit outage marks a device offline while every piece of equipment at the site is working.
So use each source for what it can observe. Netrinos is authoritative for whether a site is reachable, which is the one failure equipment inside the site cannot report, because a dark site has nothing left to send with. Monitor cameras and services from the video management system, inside the network, where they can actually be seen.
Once a day is the right cadence for an inventory. Nothing in the
device record changes faster than that except online, and
the events feed keeps a week, so a daily job never misses anything. The
whole run is two or three requests, with no state to keep between runs
beyond your own records.
Changes.
GET /devices/events?since=25h&type=added,removed.
Create or reactivate a record for each added id, fetching
GET /devices/{id} for the details. Deactivate the record
for each removed id.
curl -H "Authorization: Bearer $NETRINOS_KEY" \
"https://app.netrinos.com/api/v1/devices/events?since=25h&type=added,removed"Reconcile. GET /devices. Compare by
id against your records and correct any drift. Refresh
online, version, build, and the
other fields while you are there.
Owners, if you track them.
GET /users.
If you also want outage visibility from this API, read
online from the device list or poll
GET /devices/events?type=offline,online with a
since matching your interval. Every 15 minutes is as fast
as is useful, because that is the detection window. Monitoring inside
the site remains the better source for equipment health; see Scope.
Notes:
id. name and dnsName
change when a device is renamed, and a rename is not a create plus a
delete. A delete followed by a local login keeps the id;
see Detecting additions and removals.platform, type,
role, or event as unhandled rather than
invalid. New values can appear without a version change.Retry-After. On a 500, retry with
backoff. Do not retry a 4xx without changing the request.If your integration needs a field the API does not return, raise it with support rather than working around it.