Skip to content
Nextriv

For developers

External API

A stable, read-only server-to-server API for retrieving Nextriv devices and measurements. This page covers the complete v1 contract, copy-ready curl examples and a safe snapshot → changes synchronisation pattern.

The v1 contract at a glance

The External API is designed for backend integrations. All seven endpoints use GET, accept no request body and never modify data in Nextriv.

API base URL
https://api.nextriv.app/api/external/v1

Read-only

Identity, devices, the public metrics catalogue and measurements — with no write operations.

Server-to-server

Keep the API key on your server or in a secrets manager, never in browser code.

Stable v1

Additive changes are possible; clients should tolerate new fields without breaking the integration.

PRO plan access

Available to every organisation on the PRO plan — no waitlist, no manual approval.

Authentication and access

An API key has the form nxk_<public_id>.<secret>. It is bound to one organisation and a set of scopes.

Authorization header only

Send the API key only as Authorization: Bearer <API key>. Never place it in a URL, query string or log.

Environment variable

export NEXTRIV_API_KEY='nxk_<public_id>.<secret>'

Every curl example on this page assumes that NEXTRIV_API_KEY is already set.

  1. 1

    Enable access

    Just a PRO plan — create a key in the panel right away, no approval needed.

  2. 2

    Create an API key

    In the Nextriv panel, go to Settings → API keys, choose the scopes and create the key.

  3. 3

    Store the secret

    The full API key is shown only once. Save it immediately in a secure secrets store.

Scopes

Grant only the permissions an integration needs. The /identity endpoint reports the active scopes.

devices:read

Read the device list, device details and the device's public metrics catalogue.

measurements:read

Read latest measurements, the historical raw snapshot and the changes feed.

Endpoints

Paths are relative to the API base URL. The API exposes no sorting parameter — page order is controlled by the server and an opaque cursor.

GET/identityRequired scope: any active API key

Identity and active scopes

The simplest API key test. Returns the organisation name, API version and scopes assigned to the key.

Parameters

No parameters.

Ordering

This endpoint is not paginated and accepts no sorting parameter.

Important

  • Use it after creating or rotating an API key to confirm the organisation and permissions.
  • api_version is v1.

curl request

curl --silent --show-error \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  'https://api.nextriv.app/api/external/v1/identity'

JSON response

{
  "api_version": "v1",
  "organization_name": "ACME",
  "scopes": [
    "devices:read",
    "measurements:read"
  ]
}
GET/devicesRequired scope: devices:read

List measurement devices

Returns visible devices with a public ID, Nextriv product name, status, location, latest measurement time and metric keys.

Parameters

ParameterLocationRequiredDescription
limitqueryNoNumber of complete records requested: default 1000, minimum 1, maximum 2000.
cursorqueryNoOpaque next_cursor from the previous page.

Ordering

There is no sorting parameter. Process records in response order and continue only with the cursor returned by the API.

Important

  • status is either active or inactive.
  • When has_more=true, next_cursor leads to the next page. It is null on the final device-list page.

curl request

curl --silent --show-error --get \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'limit=1000' \
  'https://api.nextriv.app/api/external/v1/devices'

JSON response

{
  "has_more": false,
  "items": [
    {
      "id": "dev_0123456789abcdef0123456789abcdef",
      "last_measurement_at": "2026-07-10T10:00:00Z",
      "location": "A1",
      "metrics": [
        "temperature",
        "humidity"
      ],
      "name": "NX-01",
      "product_name": "Nextriv Sense Industrial",
      "status": "active"
    }
  ],
  "next_cursor": null
}
GET/devices/{device_id}Required scope: devices:read

Get one device

Returns the same public device model as a list item, but for a single device_id.

Parameters

ParameterLocationRequiredDescription
device_idpathYesPublic ID in the format dev_ followed by 32 lowercase hexadecimal characters.

Ordering

The endpoint returns one object; sorting does not apply.

Important

  • An unknown ID and an ID outside the organisation both return the same 404 resource_not_found error.
  • product_name contains only the public Nextriv product name.

curl request

curl --silent --show-error \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  'https://api.nextriv.app/api/external/v1/devices/dev_0123456789abcdef0123456789abcdef'

JSON response

{
  "id": "dev_0123456789abcdef0123456789abcdef",
  "last_measurement_at": "2026-07-10T10:00:00Z",
  "location": "A1",
  "metrics": [
    "temperature",
    "humidity"
  ],
  "name": "NX-01",
  "product_name": "Nextriv Sense Industrial",
  "status": "active"
}
GET/devices/{device_id}/metricsRequired scope: devices:read

Device metrics catalogue

Returns the metrics a device may publish through the External API, including type, unit and Polish and English names.

Parameters

ParameterLocationRequiredDescription
device_idpathYesPublic ID in the format dev_ followed by 32 lowercase hexadecimal characters.

Ordering

There is no sorting parameter. Treat key as the stable identifier, not the item's array position.

Important

  • type is number, boolean, state or string.
  • unit and decimals may be null; aggregatable describes the metric's characteristics.

curl request

curl --silent --show-error \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  'https://api.nextriv.app/api/external/v1/devices/dev_0123456789abcdef0123456789abcdef/metrics'

JSON response

{
  "device_id": "dev_0123456789abcdef0123456789abcdef",
  "metrics": [
    {
      "aggregatable": true,
      "decimals": 1,
      "key": "temperature",
      "name": {
        "en": "Temperature",
        "pl": "Temperatura"
      },
      "type": "number",
      "unit": "°C"
    },
    {
      "aggregatable": true,
      "decimals": 0,
      "key": "humidity",
      "name": {
        "en": "Humidity",
        "pl": "Wilgotność"
      },
      "type": "number",
      "unit": "%"
    }
  ]
}
GET/measurements/latestRequired scope: measurements:read

Latest visible measurements

Returns the latest visible measurement record for each matching device, or for one specified device_id.

Parameters

ParameterLocationRequiredDescription
device_idqueryNoOptionally restrict the result to one public device ID.
limitqueryNoNumber of complete records requested: default 1000, minimum 1, maximum 2000.
cursorqueryNoOpaque next_cursor from the previous page.

Ordering

There is no sorting parameter. The server defines page order; preserve it while paginating.

Important

  • Do not repeat the device_id filter when continuing with a cursor.
  • values maps each metric key to an object containing value and an optional unit.

curl request

curl --silent --show-error --get \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'device_id=dev_0123456789abcdef0123456789abcdef' \
  --data-urlencode 'limit=1000' \
  'https://api.nextriv.app/api/external/v1/measurements/latest'

JSON response

{
  "has_more": false,
  "items": [
    {
      "device_id": "dev_0123456789abcdef0123456789abcdef",
      "id": "measurement_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
      "measured_at": "2026-07-10T10:00:00Z",
      "received_at": "2026-07-10T10:00:02Z",
      "values": {
        "humidity": {
          "unit": "%",
          "value": 71.2
        },
        "temperature": {
          "unit": "°C",
          "value": 3.4
        }
      }
    }
  ],
  "next_cursor": null
}
GET/measurementsRequired scope: measurements:read

Historical raw measurement snapshot

Reads a historical, lossless snapshot. Records are not aggregated, sampled or omitted.

Parameters

ParameterLocationRequiredDescription
device_idqueryNoOptionally restrict the result to one public device ID.
fromqueryNoRange start as a date-time value that includes a timezone.
toqueryNoHistorical snapshot end as a date-time value that includes a timezone.
limitqueryNoNumber of complete records requested: default 1000, minimum 1, maximum 2000.
cursorqueryNoOpaque next_cursor from the previous page.

Ordering

There is no sorting parameter. The cursor stores a stable (measured_at, id) position; process pages in response order.

Important

  • from must not be later than to. Both timestamps must include a timezone.
  • When using cursor, do not repeat device_id, from or to — the token freezes those filters.
  • On the final page has_more=false, but next_cursor remains non-empty: it starts the changes feed.

curl request

curl --silent --show-error --get \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode 'device_id=dev_0123456789abcdef0123456789abcdef' \
  --data-urlencode 'from=2026-07-10T00:00:00Z' \
  --data-urlencode 'to=2026-07-11T00:00:00Z' \
  --data-urlencode 'limit=1000' \
  'https://api.nextriv.app/api/external/v1/measurements'

JSON response

{
  "has_more": false,
  "items": [
    {
      "device_id": "dev_0123456789abcdef0123456789abcdef",
      "id": "measurement_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
      "measured_at": "2026-07-10T10:00:00Z",
      "received_at": "2026-07-10T10:00:02Z",
      "values": {
        "humidity": {
          "unit": "%",
          "value": 71.2
        },
        "temperature": {
          "unit": "°C",
          "value": 3.4
        }
      }
    }
  ],
  "next_cursor": "cursor_snapshot_complete_example"
}
GET/measurements/changesRequired scope: measurements:read

Measurement changes feed

Continues synchronisation after a completed snapshot and also includes measurements that arrived late.

Parameters

ParameterLocationRequiredDescription
cursorqueryYesCursor from the final /measurements page or the previous changes response.
limitqueryNoNumber of complete records requested: default 1000, minimum 1, maximum 2000.

Ordering

There is no sorting parameter. Process the feed in response order and advance by change_seq.

Important

  • cursor is required. Omitting it returns 400 invalid_request with issues.
  • The feed is at-least-once; deduplicate by the measurement's public id.
  • source is live, device_history, replay or virtual.
  • Persist next_cursor even when has_more=false.

curl request

curl --silent --show-error --get \
  --header "Authorization: Bearer $NEXTRIV_API_KEY" \
  --header 'Accept: application/json' \
  --data-urlencode "cursor=$CURSOR" \
  --data-urlencode 'limit=1000' \
  'https://api.nextriv.app/api/external/v1/measurements/changes'

JSON response

{
  "has_more": false,
  "items": [
    {
      "change_seq": 12001,
      "device_id": "dev_0123456789abcdef0123456789abcdef",
      "id": "measurement_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
      "measured_at": "2026-07-10T10:00:00Z",
      "received_at": "2026-07-10T10:00:02Z",
      "source": "live",
      "values": {
        "temperature": {
          "unit": "°C",
          "value": 3.4
        }
      }
    }
  ],
  "next_cursor": "cursor_changes_12001_example"
}

Pagination and page limits

Every paginated endpoint uses an opaque cursor. Do not decode or modify it, and do not construct your own page position.

  1. 1

    1000 by default, 2000 maximum

    limit accepts values from 1 to 2000. Omitting it requests 1000 records.

  2. 2

    8 MiB maximum

    The server may return fewer records than limit to keep a page within 8 MiB. A record is never truncated.

  3. 3

    The cursor freezes context

    The token binds the organisation, resource type, filters and position. Do not move it between organisations or endpoints.

  4. 4

    Do not repeat filters

    Fetch the next snapshot page with the returned next_cursor, without repeating device_id, from or to.

  5. 5

    Check has_more

    has_more controls pagination. In raw and changes, the final next_cursor still matters for continued synchronisation.

Cursor errors and expiry

400 invalid_cursor

Malformed or forged

Invalid signature, type, organisation, phase or filters. Use only a token returned by the API.

410

Expired snapshot cursor

Snapshot cursors from /devices, /measurements/latest and /measurements can return 410 after expiry. The historical raw snapshot must complete within 24 hours.

410

Expired changes feed

The rolling changes cursor has a 30-day window. Start a new full snapshot after 410.

The snapshot → changes model

This pattern provides a lossless starting point, then keeps a local copy of measurements current without downloading the whole history again.

  1. Start a snapshot

    Call GET /measurements with optional device_id, from and to. Without to, the upper bound is fixed when the snapshot starts.

  2. Fetch every page

    While has_more=true, pass next_cursor to the next GET /measurements call and do not repeat filters.

  3. Move to changes

    When has_more=false, pass the final next_cursor to GET /measurements/changes.

  4. Continue the feed

    Process each page, persist the new cursor durably and poll again regularly, even when items is empty.

  • Persist a new cursor only after the whole page has been processed durably.
  • Deduplicate records by public id — the feed guarantees at-least-once, not exactly-once.
  • The feed includes late-arriving measurements and records beyond the historical snapshot's upper bound.
  • Change retention is 30 days. After 410, take a new snapshot instead of trying to reconstruct an old cursor.

Response headers

Every response includes a request identifier and the current limit state. Limits are counted independently per API key and per organisation.

HeaderMeaning
RateLimit-LimitMaximum requests in the currently binding policy.
RateLimit-RemainingRequests remaining in the binding window.
RateLimit-ResetSeconds until the oldest slot is released; this is not a timestamp.
RateLimit-PolicyActive policies per API key and per organisation.
X-Request-IDA req_ identifier followed by 32 lowercase hexadecimal characters, present in every response.
Retry-AfterSeconds before retrying; returned with 429 and 503.

Example headers

RateLimit-Limit: <effective-limit>
RateLimit-Remaining: <remaining>
RateLimit-Reset: <seconds>
RateLimit-Policy: <key-policy>, <organization-policy>
X-Request-ID: req_0123456789abcdef0123456789abcdef

Do not hard-code numeric limits. Read the policy and remaining budget from each response; respect Retry-After after 429.

Error contract

Errors share one stable envelope. Send request_id to support if you need help investigating a specific request.

Response shape

{
  "error": {
    "code": "string",
    "message": "string",
    "request_id": "req_..."
  },
  "issues": [
    {
      "path": "/query/limit",
      "code": "string",
      "message": "string"
    }
  ]
}

issues is optional and appears for validation failures. path uses JSON Pointer, for example /query/limit.

HTTP statuses

StatusMeaning
400Invalid parameters or cursor.
401Missing or invalid API key.
402The External API is unavailable on the current plan.
403Inactive organisation or insufficient scope.
404The device does not exist or belongs to another organisation.
410A valid cursor expired; start a new snapshot.
429Request limit exceeded; use Retry-After.
500Unexpected server error.
503Service temporarily unavailable; use Retry-After.

Error examples

Invalid API key

401 invalid_api_key

{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key is invalid.",
    "request_id": "req_0123456789abcdef0123456789abcdef"
  }
}

Resource not found

404 resource_not_found

{
  "error": {
    "code": "resource_not_found",
    "message": "Resource was not found.",
    "request_id": "req_0123456789abcdef0123456789abcdef"
  }
}

Invalid cursor

400 invalid_cursor

{
  "error": {
    "code": "invalid_cursor",
    "message": "The cursor is invalid.",
    "request_id": "req_0123456789abcdef0123456789abcdef"
  }
}

Invalid request

400 invalid_request

{
  "error": {
    "code": "invalid_request",
    "message": "Request validation failed.",
    "request_id": "req_0123456789abcdef0123456789abcdef"
  },
  "issues": [
    {
      "code": "less_than_equal",
      "message": "Input should be less than or equal to 2000",
      "path": "/query/limit"
    }
  ]
}

Need help with an External API integration?

Tell us about the planned integration and required scopes. We will help you choose a safe synchronisation model.