Endpoint reference
Every route on the ZiggyMeter REST API. For transport, authentication and worked examples, start with the REST API overview.
All routes are relative to https://<host> in client mode, or http://192.168.4.1 in Access Point mode.
Conventions
- Every route requires
Authorization: Bearer <token>exceptGET /api/v1/system/challenge,POST /api/v1/system/loginandPOST /api/v1/system/password. A long-lived token is accepted anywhere a login token is. GETreturns the current value;POSTon the same path sets it.- Fields marked write-only are accepted but never returned. Omit them to keep the stored value.
- Responses shown here are real captures from a device running firmware 1.1.
Summary
| Route | GET | POST |
|---|---|---|
/api/v1/system/challenge | Login challenge | — |
/api/v1/system/login | — | Obtain a token |
/api/v1/system/logout | — | Invalidate the token |
/api/v1/system/password | — | Change the password |
/api/v1/system/tokens | Long-lived tokens | Create a token |
/api/v1/system/tokens/<id> | — | — (DELETE revokes the token) |
/api/v1/system/version | Firmware and device identity | — |
/api/v1/system/status | Live device status | — |
/api/v1/system/clock | Clock state | Set the clock |
/api/v1/system/timezone | Timezone | Set the timezone |
/api/v1/system/power_mode | Power-saving mode | Set the mode |
/api/v1/system/reboot | — | Restart |
/api/v1/system/factory_reset | — | Restore defaults |
/api/v1/system/ota | — | Upload firmware |
/api/v1/system/certificate | HTTPS certificate | Upload a certificate |
/api/v1/system/certificate/reset | — | Restore the factory certificate |
/api/v1/system/diagnostic | Diagnostic report | — |
/api/v1/network/status | Network state | — |
/api/v1/network/scan | Nearby Wi-Fi networks | — |
/api/v1/network/wifi | — | Wi-Fi client settings |
/api/v1/network/ap | — | Access Point settings |
/api/v1/network/fallback-ap | Fallback AP state | Set fallback AP |
/api/v1/network/wifi-enabled | Wi-Fi enable state | Enable/disable Wi-Fi |
/api/v1/meter/protocol | Selected meter protocol | Select the protocol |
/api/v1/iec62056/read | Read the meter now | — |
/api/v1/iec62056/last | Last readout | — |
/api/v1/iec62056/config | IEC 62056-21 settings | Set them |
/api/v1/iec62056/schedule | Reading schedule | Set the schedule |
/api/v1/iec62056/control | Simulator and log state | Set them |
/api/v1/dlms/config | DLMS/COSEM settings | Set them |
/api/v1/dlms/obis | OBIS poll list | Set the list |
/api/v1/zigbee/config | Zigbee state | Enable/disable Zigbee |
/api/v1/zigbee/reset | — | Leave the network, re-pair |
/api/v1/zigbee/map | OBIS → ZCL mapping | Set the mapping |
/api/v1/hass | Home Assistant settings | Set them |
/api/v1/hass/send | Test publish | — |
Authentication
GET /api/v1/system/challenge
Obtain a single-use login challenge. No token required.
{
"nonce": "b61c084d57e9f599ff309ada290501eb",
"salt": "l6m|hXJGiR*8HA^Xg",
"alg": "sha256",
"expires_in_secs": 60
}| Field | Type | Description |
|---|---|---|
nonce | string | Single-use random value. Consumed on first use. |
salt | string | The account's password salt |
alg | string | Hash algorithm, currently always sha256 |
expires_in_secs | integer | How long the nonce stays valid |
POST /api/v1/system/login
Exchange a proof for a bearer token. No token required.
Request
{
"username": "admin",
"nonce": "b61c084d57e9f599ff309ada290501eb",
"proof": "e3b0c44298fc1c149afbf4c8996fb924..."
}| Field | Type | Description |
|---|---|---|
username | string | Account name |
nonce | string | The nonce from the challenge |
proof | string | SHA256( SHA256(salt ‖ password) ‖ nonce ), hex |
Response
{
"token": "RCctvdFcllLvhZAHG8J6GFNncy50y7P2rAUQWpzv",
"expires_in_secs": 1800
}INFO
Over HTTPS only, the legacy form {"username", "password"} is still accepted. It is rejected over plain HTTP.
Errors — 401 Unauthorized if the proof is wrong, or the nonce is unknown, expired or already used.
POST /api/v1/system/logout
Invalidate the current token immediately. Returns 200 OK with an empty body.
POST /api/v1/system/password
Change the account password. No token required — the proof of the old password authorises the change.
{
"nonce": "b61c084d57e9f599ff309ada290501eb",
"proof": "<proof of the old password>",
"new_hash": "<SHA256(salt ‖ new_password)>"
}| Field | Type | Description |
|---|---|---|
nonce | string | From the login challenge |
proof | string | SHA256( SHA256(salt ‖ old_password) ‖ nonce ), hex |
new_hash | string | SHA256(salt ‖ new_password), hex |
WARNING
Only a hash reaches the device, so the client must enforce the minimum password length. The device cannot check the length of something it never sees.
Over HTTPS the legacy form {"old_password", "new_password"} is also accepted.
GET /api/v1/system/tokens
List the long-lived tokens. Token values are not stored on the device, so they are never returned.
{
"max": 3,
"tokens": [
{ "id": "1a2b3c4d", "name": "Python Script", "created": 1790000000 }
]
}| Field | Type | Description |
|---|---|---|
max | integer | How many tokens can exist at once |
tokens[].id | string | Identifies the token for revoking |
tokens[].name | string | Name given at creation |
tokens[].created | integer | Unix time of creation; 0 if the clock was not set |
POST /api/v1/system/tokens
Create a token that never expires. name is 1–32 printable ASCII characters.
{ "name": "Python Script" }{
"id": "1a2b3c4d",
"name": "Python Script",
"created": 1790000000,
"token": "RCctvdFcllLvhZAHG8J6GFNncy50y7P2rAUQWpzv"
}This response is the only time token is available — store it now. Use it as Authorization: Bearer <token>. Returns 400 when the name is invalid or all max slots are in use.
Logging out with a long-lived token does not revoke it. A factory reset deletes all tokens. See Long-lived access tokens.
DELETE /api/v1/system/tokens/<id>
Revoke a token. Returns 200 OK with an empty body, or 404 for an unknown id.
curl -X DELETE https://<host>/api/v1/system/tokens/1a2b3c4d -H "Authorization: Bearer <token>"System
GET /api/v1/system/version
Firmware, interface and device identity.
{
"firmware": { "version": "1.1.675-release", "date": "Sep 6 2026", "time": "23:30:26" },
"frontend": { "version": "1.1.675-release", "date": "Sep 06 2026", "time": "23:52:06" },
"device": {
"manufacturer_id": 33919,
"device_id": 1024,
"manufacturer": "ZiggyMeter",
"model": "ZG-400",
"serial": "58E6C5384F6C"
},
"build_number": 675,
"upgrade_url": "https://fw.ziggymeter.com/ota"
}| Field | Type | Description |
|---|---|---|
firmware.version | string | Running firmware, see versioning |
frontend.version | string | Bundled web interface version |
device.serial | string | Serial number of this unit |
build_number | integer | Build number, also the Zigbee file version |
upgrade_url | string | Where the device looks for firmware |
GET /api/v1/system/status
Live status. This is what the Dashboard shows.
{
"battery_voltage": 0,
"usb_voltage": 5.09,
"battery_powered": false,
"battery_status": 0,
"device_temperature": 40.7,
"zigbee_enabled": false,
"wifi_enabled": true,
"home_assistant_enabled": false,
"power_mode": 0,
"timestamp": 1788980654,
"localtime": "2026-09-09T19:04:14+00:00",
"clock_valid": true
}| Field | Type | Description |
|---|---|---|
battery_voltage | number | Volts. 0 means no cells or fully discharged. |
usb_voltage | number | Volts on the USB-C input |
battery_powered | boolean | true when running from batteries |
device_temperature | number | °C, internal sensor |
zigbee_enabled / wifi_enabled / home_assistant_enabled | boolean | Channel enable flags |
power_mode | integer | 0 = Auto, 1 = Always save, 2 = Never save |
timestamp | integer | Unix time |
localtime | string | ISO 8601 in the configured timezone |
clock_valid | boolean | false until the clock is synchronised |
GET /api/v1/system/clock
{
"timestamp": 1788980654,
"utc": "2026-09-09T19:04:14Z",
"localtime": "2026-09-09T19:04:14+00:00",
"is_valid": true,
"timezone": "Europe/Warsaw"
}POST /api/v1/system/clock
Set the clock directly, for example from the browser's own time.
{ "timestamp": 1788980654 }GET /api/v1/system/timezone
{ "timezone": "Europe/Warsaw" }An empty string means UTC.
POST /api/v1/system/timezone
{ "timezone": "Europe/Warsaw" }The timezone determines local timestamps and the daylight-saving rule.
GET /api/v1/system/power_mode
{ "power_mode": 0 }POST /api/v1/system/power_mode
{ "power_mode": 1 }| Value | Mode | Behaviour |
|---|---|---|
0 | Auto | Sleep on battery, stay awake on USB |
1 | Always | Always sleep between readings |
2 | Never | Never sleep — see the warning |
POST /api/v1/system/reboot
Restart the device. Configuration is kept. Returns 200 OK, then the connection drops as the device restarts.
POST /api/v1/system/factory_reset
Restore factory defaults: clears Wi-Fi credentials, removes Zigbee pairing and resets the administrator password. Firmware is unaffected.
DANGER
Irreversible. After this the device is reachable only through its own Access Point.
POST /api/v1/system/ota
Upload a firmware image. The body is the raw .ota file, not JSON.
curl -X POST \
https://<host>/api/v1/system/ota \
-H "Authorization: Bearer <token>" \
--data-binary "@XXXX-YYYY-00320-20250324T195250-ZiggyMeter-1.2.320-release.ota" \
--limit-rate 128kWARNING
Rate-limit the upload. Without throttling the device cannot keep up and reports an error. --limit-rate is not optional. 128k is a tested working value — roughly 20 seconds for a 2.4 MB image. Step down if your device reports an error.
The device verifies the signature, writes the image and reboots. Rejection reasons are listed under Firmware upgrade.
GET /api/v1/system/certificate
The certificate the HTTPS server presents.
{
"custom": false,
"subject": "O=ZiggyMeter, CN=ZiggyMeter-1234",
"issuer": "O=ZiggyMeter, CN=ZiggyMeter Root CA",
"valid_from": "2026-09-27T21:30:00Z",
"valid_to": "2027-09-27T21:30:00Z",
"key_type": "EC",
"key_bits": 256,
"chain_length": 1,
"max_size": 12288,
"upload_allowed": true,
"reboot": false
}| Field | Type | Description |
|---|---|---|
custom | boolean | true when a custom certificate is installed |
subject / issuer | string | Distinguished names of the server certificate |
valid_from / valid_to | string | Validity period, ISO 8601 UTC |
key_type / key_bits | string / integer | EC or RSA, and the key size |
chain_length | integer | Certificates in the installed chain |
max_size | integer | Largest upload accepted, in bytes |
upload_allowed | boolean | false over plain HTTP, where uploads are refused |
reboot | boolean | true when the device is about to reboot to apply a change |
POST /api/v1/system/certificate
Install a custom certificate. The body is PEM text, not JSON: the certificate chain, server certificate first, followed by its unencrypted private key.
cat server.crt intermediate.crt server.key > bundle.pem
curl -X POST https://<host>/api/v1/system/certificate -H "Authorization: Bearer <token>" --data-binary @bundle.pemOn success the response is the same as GET, with "reboot": true, and the device restarts to load the certificate.
| Status | Reason |
|---|---|
400 | The file was rejected; the body is the reason as plain text, e.g. Private key does not match the (first) certificate. Nothing is changed. |
403 | Sent over plain HTTP. The upload carries a private key, so it is accepted over HTTPS only. |
Requirements are listed under HTTPS certificate.
POST /api/v1/system/certificate/reset
Restore the factory certificate. No body. The response is the same as GET; reboot is true if a custom certificate was replaced and the device is restarting, false if the factory certificate was already installed.
GET /api/v1/system/diagnostic
Download an encrypted diagnostic report as a binary blob. Intended for the manufacturer; attach it to a support request.
Enable meter transmission logging first (/iec62056/control) if the problem is meter-related.
Network
GET /api/v1/network/status
{
"ip": "192.168.4.1",
"netmask": "255.255.255.0",
"gateway": "192.168.4.1",
"hostname": "ZiggyMeter-4f6c",
"dhcp": true,
"ssid": "ZiggyMeter-4f6c",
"rssi": 0,
"channel": 1,
"authmode": 3,
"ap_mode": true,
"secure": false,
"ntp_server": "pool.ntp.org",
"wifi_enabled": true,
"wifi_temporary": false
}| Field | Type | Description |
|---|---|---|
ap_mode | boolean | true when the device hosts its own Access Point |
secure | boolean | true when the interface is being served over HTTPS |
rssi | integer | Signal strength in dBm; 0 in AP mode |
authmode | integer | Wi-Fi authentication mode |
wifi_temporary | boolean | true when Wi-Fi was brought up by hand and is not permanent |
GET /api/v1/network/scan
Scan for nearby 2.4 GHz networks. Takes several seconds — allow a 20 second timeout.
{
"hostname": "ZiggyMeter-4f6c",
"networks": [
{ "ssid": "HomeNet", "bssid": "A1B2C3D4E5F6", "rssi": -62, "channel": 4, "authmode": 3 }
]
}POST /api/v1/network/wifi
Configure the Wi-Fi client connection.
| Field | Type | Description |
|---|---|---|
ssid | string | Network name, 1–31 characters — required |
password | string | Up to 63 characters — required |
dhcp | boolean | Required. false means static addressing |
ip, netmask, gateway | string | Required when dhcp is false |
hostname | string | Optional, up to 31 characters |
apply | boolean | Optional, default false. Connect immediately |
WARNING
With apply: true the device connects to the new network right away, which drops the current connection — including the one carrying this request.
POST /api/v1/network/ap
Configure the device's own Access Point.
| Field | Type | Description |
|---|---|---|
ap_ssid | string | Optional, 1–31 characters |
ap_password | string | Optional, 8–63 characters |
ap_enabled | boolean | Optional. Enable Access Point mode |
apply | boolean | Optional, default false |
GET /api/v1/network/fallback-ap
{ "fallback_ap_enabled": true }When enabled, the device starts its own hotspot if it cannot join the configured network — your way back in when the router changes. See Wi-Fi setup.
POST /api/v1/network/fallback-ap
{ "fallback_ap_enabled": true }GET /api/v1/network/wifi-enabled
{ "wifi_enabled": true }POST /api/v1/network/wifi-enabled
{ "wifi_enabled": false }DANGER
Disabling Wi-Fi over Wi-Fi ends your session and, unless Zigbee is configured, leaves the button as the only way to bring the device back.
Meter
GET /api/v1/meter/protocol
{ "protocol": "iec62056" }Either iec62056 or dlms.
POST /api/v1/meter/protocol
{ "protocol": "dlms" }GET /api/v1/iec62056/read
Trigger a meter readout and return the result. This performs a full optical conversation before responding — allow up to 60 seconds.
Optional query parameter lang (en, de or pl) selects the language of the descriptions; anything else falls back to en.
{
"protocol": "IEC62056-21",
"meter_id": "/TST6\\2METERSIM",
"mode": "C",
"baudrate": 9600,
"timestamp": 1788980585,
"localtime": "2026-09-09T19:03:05+00:00",
"status": 0,
"readings": [
{
"obis_long": "1-0:1.8.0",
"obis": "1.8.0",
"value1": "000000004244.88*kWh",
"value2": "",
"value1_number": 4244.88,
"value1_unit": "kWh",
"medium": 1,
"channel": 0,
"billing_period": null,
"mapping_id": "07020000",
"en": "Positive active energy total"
}
]
}| Field | Type | Description |
|---|---|---|
protocol | string | IEC62056-21 or DLMS/COSEM |
meter_id | string | Identification string returned by the meter |
mode | string | Negotiated IEC 62056-21 mode |
baudrate | integer | Rate actually used |
status | integer | 0 on success |
readings[].obis_long | string | Full OBIS code with medium and channel |
readings[].obis | string | Short OBIS code |
readings[].value1 | string | Raw value exactly as the meter sent it |
readings[].value1_number | number | null | Parsed numeric value |
readings[].value1_unit | string | Unit, e.g. kWh, kW, V, A |
readings[].billing_period | integer | null | Billing period marker, if present |
readings[].mapping_id | string | null | Zigbee cluster + attribute, see mapping |
readings[].en | string | Description in the requested language |
GET /api/v1/iec62056/last
Return the last readout without contacting the meter. Same response shape as /read, and it answers immediately. Use this for polling.
GET /api/v1/iec62056/config
{
"mode_d_enabled": false,
"mode_d_baudrate": 2400,
"baudrate_max": 9600,
"battery": false,
"mode_e_dlms": false
}| Field | Type | Description |
|---|---|---|
baudrate_max | integer | Negotiation ceiling: 300–19200 |
battery | boolean | Send a wake-up sequence before each transmission |
mode_e_dlms | boolean | Continue in DLMS/COSEM when the meter announces mode E |
mode_d_enabled | boolean | Listen for unprompted mode D transmissions |
mode_d_baudrate | integer | Fixed rate used by the mode D meter |
See Meter protocols.
POST /api/v1/iec62056/config
Same fields as the GET.
GET /api/v1/iec62056/schedule
{
"enabled": true,
"period_unit": "hours",
"period_value": 4,
"trigger_hour": 0,
"trigger_minute": 0,
"trigger_day": 1
}| Field | Type | Description |
|---|---|---|
period_unit | string | minutes, hours, days or months |
period_value | integer | minutes: 1, 5, 10, 15, 30 · hours: 1, 2, 4, 8, 12 · days: 1, 7, 14, 28 · months: 1, 2, 3, 6 |
trigger_hour | integer | 0–23. Used for days and months |
trigger_minute | integer | 0–59. Used for hours, days and months |
trigger_day | integer | 1–28. Used for months only |
trigger_* fields are ignored when period_unit is minutes.
POST /api/v1/iec62056/schedule
Same fields as the GET.
GET /api/v1/iec62056/control
{ "simulator_en": true, "log_en": false }| Field | Description |
|---|---|
simulator_en | Meter simulator — generated data replaces real meter communication |
log_en | Record the raw optical conversation for the diagnostic report |
POST /api/v1/iec62056/control
Either field may be sent on its own.
{ "log_en": true }WARNING
Values produced with the simulator enabled are not real. Turn it off before drawing any conclusion about energy use.
DLMS/COSEM
Applies when the protocol is dlms, or when mode E hand-over is enabled.
GET /api/v1/dlms/config
{
"client_address": 16,
"server_logical": 1,
"server_physical": 0,
"auth": "none",
"use_encryption": false,
"password_set": false,
"receive_timeout_ms": 1000
}| Field | Type | Description |
|---|---|---|
auth | string | none, low (LLS password) or high_gmac |
use_encryption | boolean | Ciphered application layer |
client_address | integer | 16 = public/none, 32 = LLS |
server_logical | integer | Logical device inside the meter, usually 1 |
server_physical | integer | Physical meter address |
password_set | boolean | Read-only. Whether an LLS password is stored |
receive_timeout_ms | integer | Per-request receive timeout, default 1000 |
POST /api/v1/dlms/config
Accepts the same fields plus three write-only secrets. Omit any of them to keep the stored value.
| Field | Type | Description |
|---|---|---|
password | string | LLS password |
block_cipher_key | string | GUEK — 32 hex characters (16 bytes) |
auth_key | string | AK — 32 hex characters (16 bytes) |
INFO
Secrets are never returned by any endpoint. password_set is the only signal that one exists.
GET /api/v1/dlms/obis
The list of objects requested on each readout.
{
"entries": [
{ "code": [1, 0, 1, 8, 0, 255], "class_id": 3, "attribute": 2, "obis": "1-0:1.8.0" },
{ "code": [0, 0, 1, 0, 0, 255], "class_id": 8, "attribute": 2, "obis": "0-0:1.0.0" }
]
}| Field | Type | Description |
|---|---|---|
code | array | Six OBIS bytes, A.B.C.D.E.F, each 0–255 |
class_id | integer | COSEM interface class: 1 = Data, 3 = Register, 8 = Clock |
attribute | integer | Attribute index; 2 is the value for most classes |
obis | string | Read-only formatted rendering |
POST /api/v1/dlms/obis
Replaces the whole list.
{ "entries": [ { "code": [1, 0, 1, 8, 0, 255], "class_id": 3, "attribute": 2 } ] }The default list is documented under Meter protocols → OBIS poll list.
Zigbee
GET /api/v1/zigbee/config
{
"enabled": false,
"joined": true,
"channel": 11,
"short_address": 0,
"extended_pan_id": "00124b0000000000",
"pan_id": 0
}| Field | Type | Description |
|---|---|---|
enabled | boolean | Zigbee radio enabled in the stored configuration |
joined | boolean | Whether the device is joined to a network |
channel | integer | 11–26 |
short_address | integer | 16-bit address assigned by the coordinator |
extended_pan_id | string | 64-bit network identifier, 16 hex characters |
pan_id | integer | 16-bit PAN identifier |
POST /api/v1/zigbee/config
{ "enabled": true }WARNING
Changing the enabled state restarts the device.
POST /api/v1/zigbee/reset
Leave the current Zigbee network and enter joining mode. Current pairing is lost.
GET /api/v1/zigbee/map
The OBIS-to-ZCL mapping table.
TIP
For a one-off change, the web interface is easier and safer: double-click the Mapping cell in Meter → Readings. It offers only valid targets and keeps them unique. Use these endpoints for provisioning several devices identically. See Zigbee reference → mapping.
{
"mapping": [
{ "obis": "1-0:1.8.0", "mapping_id": "07020000" },
{ "obis": "1-0:12.7.0", "mapping_id": "0b040505" }
]
}mapping_id is four bytes of hex: cluster (2 bytes) followed by attribute (2 bytes). See Zigbee reference → mapping.
POST /api/v1/zigbee/map
Replaces the whole table.
{
"mapping": [
{ "obis": "1-0:15.8.0", "mode": 0, "mapping_id": "07020000" },
{ "obis": "15.8.2", "mode": 1, "mapping_id": "07020001" }
]
}| Field | Type | Description |
|---|---|---|
obis | string | OBIS code to match |
mode | integer | 0 match the full code, 1 match the short code |
mapping_id | string | 8 hex characters: cluster + attribute, or 00000000 to unassign |
Only five cluster/attribute pairs are accepted as mapping_id; anything else is rejected. They are listed under Zigbee reference → available targets.
WARNING
A POST replaces the whole table and does not enforce that each target is used by only one OBIS code — the web interface does that for you. Assigning the same mapping_id twice leaves the outcome dependent on order.
Home Assistant
GET /api/v1/hass
{
"enabled": true,
"url": "https://home-assistant.local:8123",
"cert_max_size": 10239
}url is present only when the integration is enabled. The token is never returned.
POST /api/v1/hass
| Field | Type | Description |
|---|---|---|
enabled | boolean | Enable or disable the integration |
url | string | Home Assistant URL — required when enabled |
token | string | Long-lived access token — required when enabled |
lang | string | Language for OBIS descriptions, default en |
cert | string | PEM certificate for https. Cleared if omitted while enabled |
DANGER
An https URL without a certificate will always fail to connect.
GET /api/v1/hass/send
Send a test publish to Home Assistant. This is what
calls.{ "retcode": 0, "http_status_code": 200 }retcode 0 means success; http_status_code is what Home Assistant returned. Returns 400 Bad Request if the integration is not enabled.