Skip to content

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 ​

Summary ​

RouteGETPOST
/api/v1/system/challengeLogin 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/tokensLong-lived tokensCreate a token
/api/v1/system/tokens/<id>—— (DELETE revokes the token)
/api/v1/system/versionFirmware and device identity—
/api/v1/system/statusLive device status—
/api/v1/system/clockClock stateSet the clock
/api/v1/system/timezoneTimezoneSet the timezone
/api/v1/system/power_modePower-saving modeSet the mode
/api/v1/system/reboot—Restart
/api/v1/system/factory_reset—Restore defaults
/api/v1/system/ota—Upload firmware
/api/v1/system/certificateHTTPS certificateUpload a certificate
/api/v1/system/certificate/reset—Restore the factory certificate
/api/v1/system/diagnosticDiagnostic report—
/api/v1/network/statusNetwork state—
/api/v1/network/scanNearby Wi-Fi networks—
/api/v1/network/wifi—Wi-Fi client settings
/api/v1/network/ap—Access Point settings
/api/v1/network/fallback-apFallback AP stateSet fallback AP
/api/v1/network/wifi-enabledWi-Fi enable stateEnable/disable Wi-Fi
/api/v1/meter/protocolSelected meter protocolSelect the protocol
/api/v1/iec62056/readRead the meter now—
/api/v1/iec62056/lastLast readout—
/api/v1/iec62056/configIEC 62056-21 settingsSet them
/api/v1/iec62056/scheduleReading scheduleSet the schedule
/api/v1/iec62056/controlSimulator and log stateSet them
/api/v1/dlms/configDLMS/COSEM settingsSet them
/api/v1/dlms/obisOBIS poll listSet the list
/api/v1/zigbee/configZigbee stateEnable/disable Zigbee
/api/v1/zigbee/reset—Leave the network, re-pair
/api/v1/zigbee/mapOBIS → ZCL mappingSet the mapping
/api/v1/hassHome Assistant settingsSet them
/api/v1/hass/sendTest publish—

Authentication ​

GET /api/v1/system/challenge ​

Obtain a single-use login challenge. No token required.

json
{
    "nonce": "b61c084d57e9f599ff309ada290501eb",
    "salt": "l6m|hXJGiR*8HA^Xg",
    "alg": "sha256",
    "expires_in_secs": 60
}
FieldTypeDescription
noncestringSingle-use random value. Consumed on first use.
saltstringThe account's password salt
algstringHash algorithm, currently always sha256
expires_in_secsintegerHow long the nonce stays valid

POST /api/v1/system/login ​

Exchange a proof for a bearer token. No token required.

Request

json
{
    "username": "admin",
    "nonce": "b61c084d57e9f599ff309ada290501eb",
    "proof": "e3b0c44298fc1c149afbf4c8996fb924..."
}
FieldTypeDescription
usernamestringAccount name
noncestringThe nonce from the challenge
proofstringSHA256( SHA256(salt ‖ password) ‖ nonce ), hex

Response

json
{
    "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.

json
{
    "nonce": "b61c084d57e9f599ff309ada290501eb",
    "proof": "<proof of the old password>",
    "new_hash": "<SHA256(salt ‖ new_password)>"
}
FieldTypeDescription
noncestringFrom the login challenge
proofstringSHA256( SHA256(salt ‖ old_password) ‖ nonce ), hex
new_hashstringSHA256(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.

json
{
    "max": 3,
    "tokens": [
        { "id": "1a2b3c4d", "name": "Python Script", "created": 1790000000 }
    ]
}
FieldTypeDescription
maxintegerHow many tokens can exist at once
tokens[].idstringIdentifies the token for revoking
tokens[].namestringName given at creation
tokens[].createdintegerUnix 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.

json
{ "name": "Python Script" }
json
{
    "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.

bash
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.

json
{
    "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"
}
FieldTypeDescription
firmware.versionstringRunning firmware, see versioning
frontend.versionstringBundled web interface version
device.serialstringSerial number of this unit
build_numberintegerBuild number, also the Zigbee file version
upgrade_urlstringWhere the device looks for firmware

GET /api/v1/system/status ​

Live status. This is what the Dashboard shows.

json
{
    "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
}
FieldTypeDescription
battery_voltagenumberVolts. 0 means no cells or fully discharged.
usb_voltagenumberVolts on the USB-C input
battery_poweredbooleantrue when running from batteries
device_temperaturenumber°C, internal sensor
zigbee_enabled / wifi_enabled / home_assistant_enabledbooleanChannel enable flags
power_modeinteger0 = Auto, 1 = Always save, 2 = Never save
timestampintegerUnix time
localtimestringISO 8601 in the configured timezone
clock_validbooleanfalse until the clock is synchronised

GET /api/v1/system/clock ​

json
{
    "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.

json
{ "timestamp": 1788980654 }

GET /api/v1/system/timezone ​

json
{ "timezone": "Europe/Warsaw" }

An empty string means UTC.

POST /api/v1/system/timezone ​

json
{ "timezone": "Europe/Warsaw" }

The timezone determines local timestamps and the daylight-saving rule.

GET /api/v1/system/power_mode ​

json
{ "power_mode": 0 }

POST /api/v1/system/power_mode ​

json
{ "power_mode": 1 }
ValueModeBehaviour
0AutoSleep on battery, stay awake on USB
1AlwaysAlways sleep between readings
2NeverNever 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.

bash
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 128k

WARNING

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.

json
{
    "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
}
FieldTypeDescription
custombooleantrue when a custom certificate is installed
subject / issuerstringDistinguished names of the server certificate
valid_from / valid_tostringValidity period, ISO 8601 UTC
key_type / key_bitsstring / integerEC or RSA, and the key size
chain_lengthintegerCertificates in the installed chain
max_sizeintegerLargest upload accepted, in bytes
upload_allowedbooleanfalse over plain HTTP, where uploads are refused
rebootbooleantrue 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.

bash
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.pem

On success the response is the same as GET, with "reboot": true, and the device restarts to load the certificate.

StatusReason
400The file was rejected; the body is the reason as plain text, e.g. Private key does not match the (first) certificate. Nothing is changed.
403Sent 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 ​

json
{
    "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
}
FieldTypeDescription
ap_modebooleantrue when the device hosts its own Access Point
securebooleantrue when the interface is being served over HTTPS
rssiintegerSignal strength in dBm; 0 in AP mode
authmodeintegerWi-Fi authentication mode
wifi_temporarybooleantrue 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.

json
{
    "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.

FieldTypeDescription
ssidstringNetwork name, 1–31 characters — required
passwordstringUp to 63 characters — required
dhcpbooleanRequired. false means static addressing
ip, netmask, gatewaystringRequired when dhcp is false
hostnamestringOptional, up to 31 characters
applybooleanOptional, 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.

FieldTypeDescription
ap_ssidstringOptional, 1–31 characters
ap_passwordstringOptional, 8–63 characters
ap_enabledbooleanOptional. Enable Access Point mode
applybooleanOptional, default false

GET /api/v1/network/fallback-ap ​

json
{ "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 ​

json
{ "fallback_ap_enabled": true }

GET /api/v1/network/wifi-enabled ​

json
{ "wifi_enabled": true }

POST /api/v1/network/wifi-enabled ​

json
{ "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 ​

json
{ "protocol": "iec62056" }

Either iec62056 or dlms.

POST /api/v1/meter/protocol ​

json
{ "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.

json
{
    "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"
        }
    ]
}
FieldTypeDescription
protocolstringIEC62056-21 or DLMS/COSEM
meter_idstringIdentification string returned by the meter
modestringNegotiated IEC 62056-21 mode
baudrateintegerRate actually used
statusinteger0 on success
readings[].obis_longstringFull OBIS code with medium and channel
readings[].obisstringShort OBIS code
readings[].value1stringRaw value exactly as the meter sent it
readings[].value1_numbernumber | nullParsed numeric value
readings[].value1_unitstringUnit, e.g. kWh, kW, V, A
readings[].billing_periodinteger | nullBilling period marker, if present
readings[].mapping_idstring | nullZigbee cluster + attribute, see mapping
readings[].enstringDescription 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 ​

json
{
    "mode_d_enabled": false,
    "mode_d_baudrate": 2400,
    "baudrate_max": 9600,
    "battery": false,
    "mode_e_dlms": false
}
FieldTypeDescription
baudrate_maxintegerNegotiation ceiling: 300–19200
batterybooleanSend a wake-up sequence before each transmission
mode_e_dlmsbooleanContinue in DLMS/COSEM when the meter announces mode E
mode_d_enabledbooleanListen for unprompted mode D transmissions
mode_d_baudrateintegerFixed 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 ​

json
{
    "enabled": true,
    "period_unit": "hours",
    "period_value": 4,
    "trigger_hour": 0,
    "trigger_minute": 0,
    "trigger_day": 1
}
FieldTypeDescription
period_unitstringminutes, hours, days or months
period_valueintegerminutes: 1, 5, 10, 15, 30 · hours: 1, 2, 4, 8, 12 · days: 1, 7, 14, 28 · months: 1, 2, 3, 6
trigger_hourinteger0–23. Used for days and months
trigger_minuteinteger0–59. Used for hours, days and months
trigger_dayinteger1–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 ​

json
{ "simulator_en": true, "log_en": false }
FieldDescription
simulator_enMeter simulator — generated data replaces real meter communication
log_enRecord the raw optical conversation for the diagnostic report

POST /api/v1/iec62056/control ​

Either field may be sent on its own.

json
{ "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 ​

json
{
    "client_address": 16,
    "server_logical": 1,
    "server_physical": 0,
    "auth": "none",
    "use_encryption": false,
    "password_set": false,
    "receive_timeout_ms": 1000
}
FieldTypeDescription
authstringnone, low (LLS password) or high_gmac
use_encryptionbooleanCiphered application layer
client_addressinteger16 = public/none, 32 = LLS
server_logicalintegerLogical device inside the meter, usually 1
server_physicalintegerPhysical meter address
password_setbooleanRead-only. Whether an LLS password is stored
receive_timeout_msintegerPer-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.

FieldTypeDescription
passwordstringLLS password
block_cipher_keystringGUEK — 32 hex characters (16 bytes)
auth_keystringAK — 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.

json
{
    "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" }
    ]
}
FieldTypeDescription
codearraySix OBIS bytes, A.B.C.D.E.F, each 0–255
class_idintegerCOSEM interface class: 1 = Data, 3 = Register, 8 = Clock
attributeintegerAttribute index; 2 is the value for most classes
obisstringRead-only formatted rendering

POST /api/v1/dlms/obis ​

Replaces the whole list.

json
{ "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 ​

json
{
    "enabled": false,
    "joined": true,
    "channel": 11,
    "short_address": 0,
    "extended_pan_id": "00124b0000000000",
    "pan_id": 0
}
FieldTypeDescription
enabledbooleanZigbee radio enabled in the stored configuration
joinedbooleanWhether the device is joined to a network
channelinteger11–26
short_addressinteger16-bit address assigned by the coordinator
extended_pan_idstring64-bit network identifier, 16 hex characters
pan_idinteger16-bit PAN identifier

POST /api/v1/zigbee/config ​

json
{ "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.

json
{
    "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.

json
{
    "mapping": [
        { "obis": "1-0:15.8.0", "mode": 0, "mapping_id": "07020000" },
        { "obis": "15.8.2",     "mode": 1, "mapping_id": "07020001" }
    ]
}
FieldTypeDescription
obisstringOBIS code to match
modeinteger0 match the full code, 1 match the short code
mapping_idstring8 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 ​

json
{
    "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 ​

FieldTypeDescription
enabledbooleanEnable or disable the integration
urlstringHome Assistant URL — required when enabled
tokenstringLong-lived access token — required when enabled
langstringLanguage for OBIS descriptions, default en
certstringPEM 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

TEST CONNECTION calls.
json
{ "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.