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
- Create the first account and owner with
POST /api/v1/auth/register.
- Keep the
accessToken and send it as Authorization: Bearer ....
- Register a site whose
siteId matches your MQTT topic segment.
- Publish telemetry to
storage/<siteId>/rt; read it back from /telemetry/live.
- 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
| Role | Grants |
| viewer | Read sites, telemetry, prices and own profile. |
| operator | Everything viewer can do, plus issue dispatch commands to sites. |
| admin | Everything operator can do, plus manage users, sites and API keys. |
| owner | Full 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" }
| Status | Meaning |
| 400 | Validation failed: missing field, bad type, value out of range. |
| 401 | No credentials, expired access token, or invalid API key. |
| 403 | Authenticated but the role is too low, or signup disabled. |
| 404 | Resource does not exist, or belongs to another account. |
| 409 | Conflict: email already registered, siteId already taken. |
| 429 | Rate limit exceeded. Retry after the window resets. |
| 500 | Unexpected server error. Details are logged, not returned. |
| 503 | Health 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.
| Topic | Direction | Purpose |
| storage/<siteId>/rt | inbound | Real time battery telemetry. |
| storage/<siteId>/status | inbound | Device status updates. |
| rut/<siteId>/telemetry | inbound | Gateway telemetry. |
| rut/<siteId>/heartbeat | inbound | Liveness ping. |
| storage/<siteId>/cmd | outbound | Dispatch 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:
| Metric | Accepted keys | Meaning |
| gridKw | grid_kw, gridKw, grid, p_grid | Grid power. Negative means exporting. |
| pvKw | pv_kw, pvKw, pv, solar_kw | Solar generation. |
| loadKw | load_kw, loadKw, load, p_load | Site consumption. |
| batteryKw | battery_kw, batteryKw, battery, p_batt | Battery power. Negative means discharging. |
| socPct | soc_pct, socPct, soc | State of charge, percent. |
| tempC | temp_c, tempC, temp, temperature | Battery or ambient temperature. |
| voltageV | voltage, voltage_v, v | Pack voltage. |
| currentA | current, current_a, i | Pack 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.
| Field | Type | Required | Notes |
| email | string | required | Login address. Stored lower case, must be unique. |
| password | string | required | Minimum 12 chars, mixed case and a digit. |
| accountName | string | required | Organisation name. A URL slug is derived from it. |
| name | string | optional | Display 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.
| Field | Type | Required | Notes |
| email | string | required | Registered email address. |
| password | string | required | Account 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.
| Field | Type | Required | Notes |
| refreshToken | string | required | The 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.
| Field | Type | Required | Notes |
| currentPassword | string | required | Existing password. |
| newPassword | string | required | Must 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.
| Field | Type | Required | Notes |
| name | string | optional | Organisation display name. |
| contact | object | optional | Object 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.
| Field | Type | Required | Notes |
| limit | query | optional | 1 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.
| Field | Type | Required | Notes |
| name | string | required | Label shown in the key list. |
| role | string | optional | viewer, 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.
| Field | Type | Required | Notes |
| email | string | required | Must be unique across the platform. |
| password | string | required | Must satisfy the password policy. |
| role | string | required | viewer, operator, admin or owner. |
| name | string | optional | Display 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.
| Field | Type | Required | Notes |
| name | string | optional | Display name. |
| role | string | optional | Cannot exceed your own role. |
| active | boolean | optional | false disables login immediately. |
| password | string | optional | Resets 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.
| Field | Type | Required | Notes |
| siteId | string | required | 2 to 49 chars: a-z, 0-9, dash, underscore. Globally unique. |
| name | string | required | Human readable name. |
| location | string | optional | Free text location. |
| capacityKwh | number | optional | Usable energy capacity. Default 0. |
| maxChargeKw | number | optional | Charge power limit, enforced on commands. |
| maxDischargeKw | number | optional | Discharge power limit, enforced on commands. |
| gridExportKw | number | optional | Connection export cap. |
| minSocPct | number | optional | Reserve floor. Default 5. |
| maxSocPct | number | optional | Upper limit. Default 100. |
| minExportPrice | number | optional | Price floor in EUR/MWh for automatic export. |
| timezone | string | optional | IANA 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.
| Field | Type | Required | Notes |
| action | string | required | export, charge, auto, idle or stop. |
| powerKw | number | optional | Target 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.
| Field | Type | Required | Notes |
| from | query | optional | ISO timestamp. Defaults to 24 hours ago. |
| to | query | optional | ISO timestamp. Defaults to now. |
| limit | query | optional | 1 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.
| Field | Type | Required | Notes |
| from | query | optional | ISO timestamp. Defaults to 24 hours ago. |
| to | query | optional | ISO 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 }
]
}