REST & streaming reference

OPF API

Programmatic access to accounts, users, storage sites, live battery telemetry and dispatch control. Everything is JSON over HTTPS, scoped to the account behind your credentials.

Overview

Base URLhttps://api.opf.lt
Content typeapplication/json
TimestampsISO 8601, UTC
TimezoneEurope/Vilnius

All paths are prefixed with /api/v1 except /health. Request bodies are capped at 256 kB. Every response is JSON, including errors. Resources belonging to another account return 404 rather than 403, so ids cannot be probed.

Quickstart

  1. Create the first account and owner with POST /api/v1/auth/register.
  2. Keep the accessToken and send it as Authorization: Bearer ....
  3. Register a site whose siteId matches your MQTT topic segment.
  4. Publish telemetry to storage/<siteId>/rt; read it back from /telemetry/live.
  5. Issue dispatch with POST /api/v1/sites/<siteId>/command.
# 1. Bootstrap and capture the token
TOKEN=$(curl -s -X POST https://api.opf.lt/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@opf.lt","password":"OpfDemo2026Pass"}' \
  | grep -o '"accessToken":"[^"]*"' | cut -d'"' -f4)

# 2. Register a site
curl -s -X POST https://api.opf.lt/api/v1/sites \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"siteId":"sasiai","name":"OPF Sasiai","capacityKwh":150,"maxDischargeKw":30}'

# 3. Read the fleet
curl -s https://api.opf.lt/api/v1/telemetry/summary -H "Authorization: Bearer $TOKEN"

Authentication

Two credential types are accepted. Interactive clients use short lived JWT access tokens with a rotating refresh token. Machines use long lived API keys.

Bearer token

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Access tokens expire after 15m. When you receive 401, call /auth/refresh with your refresh token to obtain a new pair. Refresh tokens last 30 days, are single use, and are stored only as hashes, so a database leak cannot be replayed.

API key

X-API-Key: opf_9Kd2xQ7bTnVw5sLpAeR3yUfHgKmZcXdQ2NvBjWtE

Keys are shown once at creation and stored hashed. They carry their own role and can be revoked instantly. Use them for dashboards, exporters and cron jobs rather than sharing a user password.

Roles

RoleGrants
viewerRead sites, telemetry, prices and own profile.
operatorEverything viewer can do, plus issue dispatch commands to sites.
adminEverything operator can do, plus manage users, sites and API keys.
ownerFull control including deleting users and sites, and account settings.

Roles are hierarchical: each includes everything below it. Nobody can grant or modify a role above their own, which prevents privilege escalation by an admin.

Errors & limits

Every failure uses the same envelope:

{ "error": "powerKw exceeds site limit of 30 kW" }
StatusMeaning
400Validation failed: missing field, bad type, value out of range.
401No credentials, expired access token, or invalid API key.
403Authenticated but the role is too low, or signup disabled.
404Resource does not exist, or belongs to another account.
409Conflict: email already registered, siteId already taken.
429Rate limit exceeded. Retry after the window resets.
500Unexpected server error. Details are logged, not returned.
503Health check only: MongoDB or MQTT is unavailable.
Global limit300 requests per minute per IP
Login limit10 attempts per 15 minutes per IP
Body limit256 kB
HeadersRateLimit-* on every response

MQTT ingest

The service subscribes to the broker and stores whatever your hardware publishes. The wildcard segment of the topic is the siteId; messages from unregistered ids are dropped, so telemetry can never land in the wrong account.

TopicDirectionPurpose
storage/<siteId>/rtinboundReal time battery telemetry.
storage/<siteId>/statusinboundDevice status updates.
rut/<siteId>/telemetryinboundGateway telemetry.
rut/<siteId>/heartbeatinboundLiveness ping.
storage/<siteId>/cmdoutboundDispatch commands, QoS 1.

Payload sample

mosquitto_pub -h emqx -p 1883 -u opf_api -P "$MQTT_PASSWORD" \
  -t storage/sasiai/rt \
  -m '{"grid_kw":0.02,"pv_kw":0,"load_kw":0.11,"battery_kw":-0.2,"soc_pct":21,"temp_c":27}'

Field normalisation

Devices name things differently. These aliases are all accepted and mapped to one shape:

MetricAccepted keysMeaning
gridKwgrid_kw, gridKw, grid, p_gridGrid power. Negative means exporting.
pvKwpv_kw, pvKw, pv, solar_kwSolar generation.
loadKwload_kw, loadKw, load, p_loadSite consumption.
batteryKwbattery_kw, batteryKw, battery, p_battBattery power. Negative means discharging.
socPctsoc_pct, socPct, socState of charge, percent.
tempCtemp_c, tempC, temp, temperatureBattery or ambient temperature.
voltageVvoltage, voltage_v, vPack voltage.
currentAcurrent, current_a, iPack current.

Unrecognised keys are preserved verbatim under payload, so nothing is lost. The latest message per site is always available instantly; history is sampled at most once per minute and expires after 90 days.

System

GET /health None

Liveness and dependency check. Returns 503 when MongoDB or MQTT is down, so it can drive a load balancer or uptime monitor.

Request

curl -s https://api.opf.lt/health

Response

{
  "status": "ok",
  "service": "opf-api",
  "ts": "2026-08-13T20:23:45.834Z",
  "mongo": true,
  "mqtt": {
    "enabled": true,
    "connected": true,
    "topics": ["storage/+/rt", "storage/+/status", "rut/+/telemetry"]
  }
}
GET /api/v1/meta None

Service metadata and a machine readable list of every route.

Request

curl -s https://api.opf.lt/api/v1/meta

Response

{
  "service": "opf-api",
  "version": "1.0.0",
  "timezone": "Europe/Vilnius",
  "endpoints": ["POST   /api/v1/auth/register", "POST   /api/v1/auth/login", "..."]
}

Authentication

POST /api/v1/auth/register None

Creates an account and its first owner user. Allowed only when no users exist yet (first run bootstrap) or when ALLOW_SIGNUP is enabled. Password must be at least 12 characters and contain lower case, upper case and a digit.

FieldTypeRequiredNotes
emailstringrequiredLogin address. Stored lower case, must be unique.
passwordstringrequiredMinimum 12 chars, mixed case and a digit.
accountNamestringrequiredOrganisation name. A URL slug is derived from it.
namestringoptionalDisplay name. Defaults to the email.

Request

curl -s -X POST https://api.opf.lt/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "admin@opf.lt",
    "password": "OpfDemo2026Pass",
    "name": "Admin",
    "accountName": "OPF"
  }'

Response

{
  "user": {
    "id": "6a7e2814d2f9fdca48904304",
    "accountId": "6a7e2813d2f9fdca48904303",
    "email": "admin@opf.lt",
    "name": "Admin",
    "role": "owner",
    "active": true,
    "createdAt": "2026-08-13T20:24:51.796Z",
    "lastLoginAt": null
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "K9mZ3Xq...",
  "expiresAt": "2026-09-12T20:24:51.812Z"
}

Error responses

409 Conflict
{ "error": "email already registered" }

403 Forbidden
{ "error": "signup is disabled" }
POST /api/v1/auth/login None

Exchanges credentials for an access token (15 minutes) and a refresh token (30 days). Limited to 10 attempts per 15 minutes per IP. Unknown email and wrong password return the same error.

FieldTypeRequiredNotes
emailstringrequiredRegistered email address.
passwordstringrequiredAccount password.

Request

curl -s -X POST https://api.opf.lt/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@opf.lt", "password": "OpfDemo2026Pass"}'

Response

{
  "user": {
    "id": "6a7e2814d2f9fdca48904304",
    "accountId": "6a7e2813d2f9fdca48904303",
    "email": "admin@opf.lt",
    "name": "Admin",
    "role": "owner",
    "active": true,
    "createdAt": "2026-08-13T20:24:51.796Z",
    "lastLoginAt": "2026-08-13T21:02:10.114Z"
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "K9mZ3Xq...",
  "expiresAt": "2026-09-12T21:02:10.130Z"
}

Error responses

401 Unauthorized
{ "error": "invalid credentials" }

429 Too Many Requests
{ "error": "too many attempts, try again later" }
POST /api/v1/auth/refresh Refresh token

Rotates the refresh token and returns a fresh access token. Refresh tokens are single use: the old one is destroyed, so replaying a stolen token fails.

FieldTypeRequiredNotes
refreshTokenstringrequiredThe token returned by login or a previous refresh.

Request

curl -s -X POST https://api.opf.lt/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "K9mZ3Xq..."}'

Response

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "Pq7Rt2...",
  "expiresAt": "2026-09-12T21:14:03.552Z"
}

Error responses

401 Unauthorized
{ "error": "invalid or expired refresh token" }
POST /api/v1/auth/logout None

Destroys the supplied refresh token. Always returns success so it cannot be used to probe token validity.

Request

curl -s -X POST https://api.opf.lt/api/v1/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "K9mZ3Xq..."}'

Response

{ "ok": true }
GET /api/v1/auth/me Bearer or API key

Returns the caller identity and their account. With an API key it reports the key role instead of a user.

Request

curl -s https://api.opf.lt/api/v1/auth/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "user": {
    "id": "6a7e2814d2f9fdca48904304",
    "accountId": "6a7e2813d2f9fdca48904303",
    "email": "admin@opf.lt",
    "name": "Admin",
    "role": "owner",
    "active": true,
    "createdAt": "2026-08-13T20:24:51.796Z",
    "lastLoginAt": "2026-08-13T21:02:10.114Z"
  },
  "account": {
    "id": "6a7e2813d2f9fdca48904303",
    "name": "OPF",
    "slug": "opf",
    "plan": "standard"
  }
}
POST /api/v1/auth/password Bearer

Changes your own password and signs out every other session. Not available to API keys.

FieldTypeRequiredNotes
currentPasswordstringrequiredExisting password.
newPasswordstringrequiredMust satisfy the password policy.

Request

curl -s -X POST https://api.opf.lt/api/v1/auth/password \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"currentPassword": "OpfDemo2026Pass", "newPassword": "NewOpfPass2026x"}'

Response

{ "ok": true, "message": "password updated, other sessions signed out" }

Accounts

GET /api/v1/accounts/current Bearer or API key

Your account profile plus usage counters.

Request

curl -s https://api.opf.lt/api/v1/accounts/current \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "account": {
    "id": "6a7e2813d2f9fdca48904303",
    "name": "OPF",
    "slug": "opf",
    "plan": "standard",
    "active": true,
    "createdAt": "2026-08-13T20:24:51.780Z",
    "contact": { "email": "info@opf.systems", "phone": "", "address": "Kalviai, Lithuania" }
  },
  "usage": { "users": 3, "sites": 8 }
}
PATCH /api/v1/accounts/current owner

Updates the account display name and contact block.

FieldTypeRequiredNotes
namestringoptionalOrganisation display name.
contactobjectoptionalObject with email, phone and address strings.

Request

curl -s -X PATCH https://api.opf.lt/api/v1/accounts/current \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OPF Energy",
    "contact": { "email": "info@opf.systems", "address": "Kalviai, Lithuania" }
  }'

Response

{ "account": { "id": "6a7e2813d2f9fdca48904303", "name": "OPF Energy", "slug": "opf" } }
GET /api/v1/accounts/current/audit admin

Audit trail of account changes: logins, user and site changes, commands, API key lifecycle. Retained 365 days.

FieldTypeRequiredNotes
limitqueryoptional1 to 500. Default 100.

Request

curl -s "https://api.opf.lt/api/v1/accounts/current/audit?limit=5" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "entries": [
    { "ts": "2026-08-13T21:05:11.004Z", "action": "site.command", "actorId": "6a7e2814d2f9fdca48904304", "target": "sasiai" },
    { "ts": "2026-08-13T21:04:02.881Z", "action": "site.created", "actorId": "6a7e2814d2f9fdca48904304", "target": "sasiai" },
    { "ts": "2026-08-13T21:02:10.120Z", "action": "auth.login", "actorId": "6a7e2814d2f9fdca48904304", "target": null }
  ]
}

API keys

GET /api/v1/accounts/current/api-keys admin

Lists active keys. Only the prefix is stored in readable form; the secret cannot be recovered.

Request

curl -s https://api.opf.lt/api/v1/accounts/current/api-keys \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "apiKeys": [
    {
      "id": "6a7e2999d2f9fdca48904311",
      "name": "grafana-readonly",
      "prefix": "opf_9Kd2xQ",
      "role": "viewer",
      "createdAt": "2026-08-13T21:10:44.201Z",
      "lastUsedAt": "2026-08-13T21:12:03.559Z"
    }
  ]
}
POST /api/v1/accounts/current/api-keys admin

Creates a machine credential for dashboards or scripts. The plaintext key is returned exactly once. Send it later in the X-API-Key header. You cannot grant a role above your own.

FieldTypeRequiredNotes
namestringrequiredLabel shown in the key list.
rolestringoptionalviewer, operator or admin. Default operator.

Request

curl -s -X POST https://api.opf.lt/api/v1/accounts/current/api-keys \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "grafana-readonly", "role": "viewer"}'

Response

{
  "apiKey": { "id": "6a7e2999d2f9fdca48904311", "name": "grafana-readonly", "prefix": "opf_9Kd2xQ", "role": "viewer" },
  "key": "opf_9Kd2xQ7bTnVw5sLpAeR3yUfHgKmZcXdQ2NvBjWtE"
}
DELETE /api/v1/accounts/current/api-keys/:id admin

Revokes a key immediately. Revoked keys are kept for audit purposes but never authenticate again.

Request

curl -s -X DELETE https://api.opf.lt/api/v1/accounts/current/api-keys/6a7e2999d2f9fdca48904311 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{ "ok": true }

Users

GET /api/v1/users admin

Lists users in your account. Password hashes are never returned.

Request

curl -s https://api.opf.lt/api/v1/users \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "users": [
    {
      "id": "6a7e2814d2f9fdca48904304",
      "accountId": "6a7e2813d2f9fdca48904303",
      "email": "admin@opf.lt",
      "name": "Admin",
      "role": "owner",
      "active": true,
      "createdAt": "2026-08-13T20:24:51.796Z",
      "lastLoginAt": "2026-08-13T21:02:10.114Z"
    }
  ]
}
POST /api/v1/users admin

Invites a user into your account with a starting password. You cannot create a user with a role above your own.

FieldTypeRequiredNotes
emailstringrequiredMust be unique across the platform.
passwordstringrequiredMust satisfy the password policy.
rolestringrequiredviewer, operator, admin or owner.
namestringoptionalDisplay name.

Request

curl -s -X POST https://api.opf.lt/api/v1/users \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "operator@opf.lt",
    "password": "FieldOps2026Key",
    "name": "Field Operator",
    "role": "operator"
  }'

Response

{
  "user": {
    "id": "6a7e2a55d2f9fdca48904308",
    "accountId": "6a7e2813d2f9fdca48904303",
    "email": "operator@opf.lt",
    "name": "Field Operator",
    "role": "operator",
    "active": true,
    "createdAt": "2026-08-13T21:20:31.442Z",
    "lastLoginAt": null
  }
}

Error responses

403 Forbidden
{ "error": "cannot grant a role above your own" }
GET /api/v1/users/:id admin

Fetches one user from your account. Users in other accounts return 404, never 403.

Request

curl -s https://api.opf.lt/api/v1/users/6a7e2a55d2f9fdca48904308 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{ "user": { "id": "6a7e2a55d2f9fdca48904308", "email": "operator@opf.lt", "role": "operator", "active": true } }
PATCH /api/v1/users/:id admin

Updates name, role, active flag or password. Users above your own role cannot be modified.

FieldTypeRequiredNotes
namestringoptionalDisplay name.
rolestringoptionalCannot exceed your own role.
activebooleanoptionalfalse disables login immediately.
passwordstringoptionalResets the password.

Request

curl -s -X PATCH https://api.opf.lt/api/v1/users/6a7e2a55d2f9fdca48904308 \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "admin", "active": true}'

Response

{ "user": { "id": "6a7e2a55d2f9fdca48904308", "email": "operator@opf.lt", "role": "admin", "active": true } }
DELETE /api/v1/users/:id owner

Removes a user and all of their sessions. You cannot delete yourself.

Request

curl -s -X DELETE https://api.opf.lt/api/v1/users/6a7e2a55d2f9fdca48904308 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{ "ok": true }

Sites

GET /api/v1/sites viewer

Lists every site in your account with its latest telemetry attached. A site counts as online when telemetry arrived within the stale window (default 300 seconds).

Request

curl -s https://api.opf.lt/api/v1/sites \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "sites": [
    {
      "siteId": "sasiai",
      "name": "OPF Sasiai",
      "location": "Kalviai",
      "capacityKwh": 150,
      "maxChargeKw": 30,
      "maxDischargeKw": 30,
      "gridExportKw": 30,
      "minSocPct": 5,
      "maxSocPct": 100,
      "mode": "auto",
      "minExportPrice": 115,
      "timezone": "Europe/Vilnius",
      "active": true,
      "createdAt": "2026-08-13T21:04:02.870Z",
      "online": true,
      "latest": {
        "ts": "2026-08-13T21:25:07.285Z",
        "metrics": { "gridKw": 0.02, "pvKw": 0, "loadKw": 0.11, "batteryKw": -0.2, "socPct": 21, "tempC": 27 }
      }
    }
  ]
}
POST /api/v1/sites admin

Registers a site. The siteId must match the wildcard segment your hardware publishes on, for example storage/sasiai/rt. Telemetry from unregistered site ids is discarded.

FieldTypeRequiredNotes
siteIdstringrequired2 to 49 chars: a-z, 0-9, dash, underscore. Globally unique.
namestringrequiredHuman readable name.
locationstringoptionalFree text location.
capacityKwhnumberoptionalUsable energy capacity. Default 0.
maxChargeKwnumberoptionalCharge power limit, enforced on commands.
maxDischargeKwnumberoptionalDischarge power limit, enforced on commands.
gridExportKwnumberoptionalConnection export cap.
minSocPctnumberoptionalReserve floor. Default 5.
maxSocPctnumberoptionalUpper limit. Default 100.
minExportPricenumberoptionalPrice floor in EUR/MWh for automatic export.
timezonestringoptionalIANA zone. Defaults to the service timezone.

Request

curl -s -X POST https://api.opf.lt/api/v1/sites \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "siteId": "sasiai",
    "name": "OPF Sasiai",
    "location": "Kalviai",
    "capacityKwh": 150,
    "maxChargeKw": 30,
    "maxDischargeKw": 30,
    "gridExportKw": 30,
    "minSocPct": 5,
    "minExportPrice": 115
  }'

Response

{
  "site": {
    "siteId": "sasiai",
    "name": "OPF Sasiai",
    "capacityKwh": 150,
    "maxChargeKw": 30,
    "maxDischargeKw": 30,
    "mode": "auto",
    "active": true,
    "online": false,
    "latest": null
  }
}

Error responses

409 Conflict
{ "error": "siteId already exists" }

400 Bad Request
{ "error": "siteId must be 2-49 chars of a-z, 0-9, dash or underscore" }
GET /api/v1/sites/:siteId viewer

One site with its latest telemetry.

Request

curl -s https://api.opf.lt/api/v1/sites/sasiai \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "site": {
    "siteId": "sasiai",
    "name": "OPF Sasiai",
    "capacityKwh": 150,
    "mode": "auto",
    "online": true,
    "latest": {
      "ts": "2026-08-13T21:25:07.285Z",
      "metrics": { "gridKw": 0.02, "socPct": 21, "batteryKw": -0.2 }
    }
  }
}
PATCH /api/v1/sites/:siteId admin

Updates site configuration. Send only the fields you want changed.

Request

curl -s -X PATCH https://api.opf.lt/api/v1/sites/sasiai \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"minExportPrice": 120, "maxDischargeKw": 28}'

Response

{ "site": { "siteId": "sasiai", "minExportPrice": 120, "maxDischargeKw": 28, "mode": "auto" } }
DELETE /api/v1/sites/:siteId owner

Deletes the site and its latest telemetry record. Historical points expire on their own retention schedule.

Request

curl -s -X DELETE https://api.opf.lt/api/v1/sites/sasiai \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{ "ok": true }
POST /api/v1/sites/:siteId/command operator

Publishes a dispatch command to storage/{siteId}/cmd at QoS 1 and records the new mode. powerKw is validated against the site charge or discharge limit before publishing. Returns 202 because the device acknowledges asynchronously.

FieldTypeRequiredNotes
actionstringrequiredexport, charge, auto, idle or stop.
powerKwnumberoptionalTarget power. Must not exceed the matching site limit.

Request

curl -s -X POST https://api.opf.lt/api/v1/sites/sasiai/command \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "export", "powerKw": 25}'

Response

202 Accepted
{
  "ok": true,
  "topic": "storage/sasiai/cmd",
  "command": {
    "action": "export",
    "powerKw": 25,
    "issuedAt": "2026-08-13T20:25:09.620Z",
    "issuedBy": "6a7e2814d2f9fdca48904304",
    "requestId": "sasiai-1786652709620"
  }
}

Error responses

400 Bad Request
{ "error": "powerKw exceeds site limit of 30 kW" }

400 Bad Request
{ "error": "action must be one of: export, charge, auto, idle, stop" }

Telemetry

GET /api/v1/telemetry/live viewer

Latest reading for every site in your account. Ideal for a dashboard poll every few seconds.

Request

curl -s https://api.opf.lt/api/v1/telemetry/live \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "ts": "2026-08-13T20:25:09.501Z",
  "sites": [
    {
      "siteId": "sasiai",
      "ts": "2026-08-13T20:25:07.285Z",
      "online": true,
      "metrics": { "gridKw": 0.02, "pvKw": 0, "loadKw": 0.11, "batteryKw": -0.2, "socPct": 21, "tempC": 27 }
    }
  ]
}
GET /api/v1/telemetry/live/:siteId viewer

Latest reading for one site, including the raw device payload exactly as published.

Request

curl -s https://api.opf.lt/api/v1/telemetry/live/sasiai \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "siteId": "sasiai",
  "ts": "2026-08-13T20:25:07.285Z",
  "metrics": { "gridKw": 0.02, "pvKw": 0, "loadKw": 0.11, "batteryKw": -0.2, "socPct": 21, "tempC": 27 },
  "payload": { "grid_kw": 0.02, "pv_kw": 0, "load_kw": 0.11, "battery_kw": -0.2, "soc_pct": 21, "temp_c": 27 }
}

Error responses

404 Not Found
{ "error": "no telemetry received yet" }
GET /api/v1/telemetry/history/:siteId viewer

Time series for one site, ascending by timestamp. History is sampled at most once per minute per site and retained for 90 days by default.

FieldTypeRequiredNotes
fromqueryoptionalISO timestamp. Defaults to 24 hours ago.
toqueryoptionalISO timestamp. Defaults to now.
limitqueryoptional1 to 5000. Default 1000.

Request

curl -s "https://api.opf.lt/api/v1/telemetry/history/sasiai?from=2026-08-13T00:00:00Z&to=2026-08-13T23:59:59Z&limit=500" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "siteId": "sasiai",
  "from": "2026-08-13T00:00:00.000Z",
  "to": "2026-08-13T23:59:59.000Z",
  "count": 2,
  "points": [
    { "ts": "2026-08-13T20:24:07.101Z", "metrics": { "gridKw": 0.01, "socPct": 21, "batteryKw": -0.18 } },
    { "ts": "2026-08-13T20:25:07.285Z", "metrics": { "gridKw": 0.02, "socPct": 21, "batteryKw": -0.2 } }
  ]
}

Error responses

400 Bad Request
{ "error": "from must be before to" }
GET /api/v1/telemetry/summary viewer

Fleet roll up: how many sites are online, aggregate capacity, stored energy derived from each site SOC, and combined power flows.

Request

curl -s https://api.opf.lt/api/v1/telemetry/summary \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "ts": "2026-08-13T20:25:09.550Z",
  "sites": 8,
  "online": 7,
  "capacityKwh": 1211.5,
  "storedKwh": 402.31,
  "averageSocPct": 33.2,
  "batteryKw": -73.14,
  "pvKw": 0.01,
  "gridKw": -64.64
}
GET /api/v1/telemetry/stream viewer

Server sent events. Every ingested message for your account is pushed immediately, with a comment ping every 25 seconds to hold the connection open. Only messages from your own sites are delivered.

Request

curl -N -s https://api.opf.lt/api/v1/telemetry/stream \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

event: ready
data: {"ts":"2026-08-13T20:25:00.001Z"}

event: telemetry
data: {"siteId":"sasiai","ts":"2026-08-13T20:25:07.285Z","metrics":{"gridKw":0.02,"socPct":21}}

: ping
GET /api/v1/telemetry/prices viewer

Market prices from the shared price collection, used by the optimiser to choose export windows.

FieldTypeRequiredNotes
fromqueryoptionalISO timestamp. Defaults to 24 hours ago.
toqueryoptionalISO timestamp. Defaults to now.

Request

curl -s "https://api.opf.lt/api/v1/telemetry/prices?from=2026-08-14T00:00:00Z&to=2026-08-15T00:00:00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "collection": "price_market",
  "from": "2026-08-14T00:00:00.000Z",
  "to": "2026-08-15T00:00:00.000Z",
  "count": 2,
  "points": [
    { "start": "2026-08-14T08:00:00.000Z", "end": "2026-08-14T08:15:00.000Z", "price": 177.07 },
    { "start": "2026-08-14T09:00:00.000Z", "end": "2026-08-14T09:15:00.000Z", "price": 174.07 }
  ]
}