Gurglee Premium

Push readings to Gurglee from your own setup

If you already have a probe reading temperature or pH, there is no reason to type those numbers in again. Gurglee gives you an address and a key: anything that can make an HTTP call can write to it.

Read this before anything else: this is not a SmartLife or Tuya integration. Those services want a commercial contract for production use, and for us to go and read your sensors we would have to hold your credentials for those clouds — more responsibility than we are willing to take on for a few euros a month.

What you get instead works with any system: Home Assistant, Node-RED, ESPHome, an aquarium controller, a script on a Raspberry Pi, even an automation that starts in the SmartLife app itself. The bit that connects your sensor to Gurglee is yours to write — the ready-made examples are below, and it is about five lines.

1 · Getting a key

You need Premium and a Gurglee account (readings land in your profile, and travel from there to all your devices).

  1. In the app: Menu → Tools → “API and integrations”.
  2. Turn the API on switch on.
  3. Tap Create a key and give it a name that reminds you where it went (“Home Assistant”, “Living room ESP”).
  4. Copy it right away. You see it once: the server keeps only a fingerprint, so we cannot show it to you again. Lose it and you revoke it and make another.

The key starts with glk_ and goes in the Authorization header, like an ordinary bearer token. You can have up to 5 active at a time: one per system, so revoking one does not stop the others.

The same screen has a “Try the call” button: it sends a real request with your real key, but stores nothing. It is the quickest way to find out you mistyped the tank's name, instead of digging through your Home Assistant logs.

2 · The endpoints

Base: https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1

GET /tanks — what can I write to

The call to make first: it tells you what your tanks are called, which parameters they track, in what units, and the limits in force.

curl -H "Authorization: Bearer glk_YOUR_KEY" \
  https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/tanks
{
  "ok": true,
  "tanks": [
    {
      "id": "0f0b…",
      "name": "Reef",
      "type": "reef",
      "volumeLiters": 300,
      "parameters": [
        { "key": "temperature", "unit": "°C", "accepts": ["°C", "°F", "K"] },
        { "key": "ph",          "unit": "pH", "accepts": ["pH"] }
      ]
    }
  ],
  "limits": { "requestsPerDay": 500, "minIntervalMinutes": { "temperature": 60, "default": 1440 } }
}

POST /readings — sending a reading

FieldRequiredWhat it is
tankyesThe tank's id, name or an alias. The name is handier; if two tanks share it the request is turned down with the list of ids, rather than guessing.
parameteryesThe parameter key (temperature, ph, kh…): the list is in the table below. Custom parameters you created yourself work too.
valueyesThe number. A string is accepted as well, comma decimals included ("24,6"), because that is what a template with an Italian locale produces.
unitnoLeft out, the parameter's canonical unit is assumed. Written forms are generous: C, celsius, °C, mg/L, uS/cm.
measuredAtnoWhen it was taken, ISO 8601 (2026-08-11T18:30:00Z) or an epoch. Left out, it is now.
notenoUp to 500 characters, as in the app's form.
dryRunnotrue checks everything and stores nothing. It is what the try button uses.

A successful write answers 201:

{
  "ok": true,
  "message": "Reading stored. It will show up in the app at the next sync.",
  "reading": {
    "id": "…", "tank": { "id": "0f0b…", "name": "Reef" },
    "parameter": "temperature", "value": 77, "unit": "°F",
    "valueCanonical": 25, "canonicalUnit": "°C",
    "measuredAt": "2026-08-11T18:30:00.000Z", "minIntervalMinutes": 60
  }
}

As you can see the value is converted into the parameter's canonical unit, exactly as when you type it in the app: go ahead and send Fahrenheit if that is what your sensor speaks.

3 · Parameters and units

KeyCanonicalUnits accepted
temperature°C°C, °F, K
phpHpH
ammonia, nitrite, nitrateppmppm (= mg/L)
khdKHdKH, meq/L, ppm
ghdGHdGH, ppm, meq/L
tdsppmppm, µS/cm, mS/cm
salinitypptppt, SG, mS/cm
orpmVmV
calcium, magnesium, phosphate, co2, iron, potassium, silicate, copper, oxygenppmppm

A custom parameter created in the app works with its own key and its own unit: we know no conversions for it, so that is the only one it accepts.

The tank does not need to track that parameter for the reading to be accepted — but to see it in the charts and the overview, add it to the tank's tracked parameters.

4 · How often you can send

ParameterOne reading every
temperature60 minutes
everything else24 hours

This is not about saving disk space: a value every ten seconds adds no information, fills up the history and makes charts unreadable. Temperature is the exception because it is the one value a probe really does measure continuously, and whose shape across the day is worth seeing.

Send too early and you get a 429 saying how many seconds are left (also in the Retry-After header): your automation can simply try again next time round. The count looks at API readings only: what you type into the app does not block the sensor.

There is also a cap of 500 requests a day per key, valid or not. It is there to stop a runaway loop, not to limit normal use: with the intervals above, a well-instrumented tank uses a few dozen.

5 · Ready-made examples

Terminal (curl)

curl -X POST https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/readings \
  -H "Authorization: Bearer glk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tank": "Reef", "parameter": "temperature", "value": 25.4, "unit": "°C"}'

Home Assistant

In configuration.yaml, a rest_command plus an automation calling it every hour:

rest_command:
  gurglee_temperature:
    url: "https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/readings"
    method: POST
    headers:
      Authorization: "Bearer glk_YOUR_KEY"
      Content-Type: "application/json"
    payload: >-
      {"tank": "Reef", "parameter": "temperature",
       "value": {{ states("sensor.aquarium_temperature") }} }

automation:
  - alias: "Gurglee — temperature every hour"
    trigger:
      - platform: time_pattern
        hours: "/1"
    condition:
      # Don't send "unknown" when the sensor is not answering: it would be
      # turned down with a 400 and fill your log with errors.
      - condition: template
        value_template: "{{ states('sensor.aquarium_temperature') not in ['unknown', 'unavailable'] }}"
    action:
      - service: rest_command.gurglee_temperature

Node-RED

A function node that builds the message, followed by an http request (POST) to the endpoint:

msg.headers = {
  "Authorization": "Bearer glk_YOUR_KEY",
  "Content-Type": "application/json"
};
msg.payload = {
  tank: "Reef",
  parameter: "temperature",
  value: msg.payload            // the number coming from the sensor
};
return msg;

Python / Raspberry Pi

import requests

requests.post(
    "https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/readings",
    headers={"Authorization": "Bearer glk_YOUR_KEY"},
    json={"tank": "Reef", "parameter": "ph", "value": 8.12},
    timeout=20,
)

ESPHome

Straight from the ESP, without going through Home Assistant:

http_request:
  useragent: esphome/gurglee
  timeout: 10s

sensor:
  - platform: dallas_temp
    name: "Aquarium"
    id: aquarium_temp
    update_interval: 60min
    on_value:
      - http_request.post:
          url: "https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/readings"
          headers:
            Authorization: "Bearer glk_YOUR_KEY"
            Content-Type: "application/json"
          json:
            tank: "Reef"
            parameter: "temperature"
            value: !lambda "return id(aquarium_temp).state;"

PowerShell

Invoke-RestMethod -Method Post `
  -Uri "https://cyykgbgeswcflhxjgsag.supabase.co/functions/v1/api/v1/readings" `
  -Headers @{ Authorization = "Bearer glk_YOUR_KEY" } `
  -ContentType "application/json" `
  -Body '{"tank":"Reef","parameter":"temperature","value":25.4}'

6 · SmartLife, Tuya and the like

It is the first question people ask, so here is the long answer.

Why there is no direct integration. The free tier of Tuya's cloud APIs — the ones behind SmartLife and most cheap WiFi thermometers, Inkbird included — is explicitly “for development only”: production use needs a commercial contract, at a price that makes no sense for an app costing a few euros a month. And even if we could pay for it, reading your sensors would mean keeping your credentials for that cloud on our servers: more sensitive data than the whole of Gurglee put together.

How to close the gap. You need something in the middle, and it is usually already in the house:

If direct integration ever becomes sustainable we will build it, and nobody who used the API in the meantime loses anything: the readings are already inside Gurglee, in the same shape.

7 · Errors

Every error is JSON with a stable error (for your code) and a readable message (for you, looking at the logs).

HTTPerrorWhat to do
401missing_tokenThe Authorization header is missing.
401invalid_tokenUnknown or revoked key. Make another one in the app.
403api_disabledThe switch is off: turn it back on in “API and integrations”.
403premium_requiredThe subscription tied to that key is not active.
400unknown_tank · ambiguous_tankThe response lists your tanks with their ids: use the id.
400unknown_parameter · bad_unit · bad_valueThe response says what that parameter accepts.
400bad_measured_atUnreadable date, in the future, or over a year old (usually an epoch in seconds read as milliseconds).
429too_soonToo early for that parameter: retryAfterSeconds says how long is left.
429daily_capPast 500 requests for the day. The counter resets at midnight UTC.
500server_errorOur fault: try again later.

8 · Security and privacy

Something not adding up, or did you hook up a system that is not listed here and would like to see it appear? Write to us: [email protected].