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).
- In the app: Menu → Tools → “API and integrations”.
- Turn the API on switch on.
- Tap Create a key and give it a name that reminds you where it went (“Home Assistant”, “Living room ESP”).
- 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
| Field | Required | What it is |
|---|---|---|
tank | yes | The 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. |
parameter | yes | The parameter key (temperature, ph, kh…): the list is in the table below. Custom parameters you created yourself work too. |
value | yes | The number. A string is accepted as well, comma decimals included ("24,6"), because that is what a template with an Italian locale produces. |
unit | no | Left out, the parameter's canonical unit is assumed. Written forms are generous: C, celsius, °C, mg/L, uS/cm. |
measuredAt | no | When it was taken, ISO 8601 (2026-08-11T18:30:00Z) or an epoch. Left out, it is now. |
note | no | Up to 500 characters, as in the app's form. |
dryRun | no | true 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
| Key | Canonical | Units accepted |
|---|---|---|
temperature | °C | °C, °F, K |
ph | pH | pH |
ammonia, nitrite, nitrate | ppm | ppm (= mg/L) |
kh | dKH | dKH, meq/L, ppm |
gh | dGH | dGH, ppm, meq/L |
tds | ppm | ppm, µS/cm, mS/cm |
salinity | ppt | ppt, SG, mS/cm |
orp | mV | mV |
calcium, magnesium, phosphate, co2, iron, potassium, silicate, copper, oxygen | ppm | ppm |
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
| Parameter | One reading every |
|---|---|
temperature | 60 minutes |
| everything else | 24 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:
- Home Assistant — if you have it, your Tuya/SmartLife devices are already in there (official integration or LocalTuya). From that point the block in section 5 is all it takes. This is the route we recommend: set it up once and it keeps working, even without the manufacturer's cloud when you use LocalTuya.
- Node-RED with the
node-red-contrib-tuya-smart-devicenodes: reads the sensor on your LAN and calls Gurglee. - A script using
tinytuya(Python) on a Raspberry Pi or an always-on PC, run hourly by cron. - The SmartLife app's own automations, if your model exposes “webhooks” in scenes: point one at our endpoint and you are done, no extra hardware.
- Aquarium controllers (Apex, GHL, Seneye…): nearly all of them can do a POST, or expose a JSON a script can read.
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).
| HTTP | error | What to do |
|---|---|---|
| 401 | missing_token | The Authorization header is missing. |
| 401 | invalid_token | Unknown or revoked key. Make another one in the app. |
| 403 | api_disabled | The switch is off: turn it back on in “API and integrations”. |
| 403 | premium_required | The subscription tied to that key is not active. |
| 400 | unknown_tank · ambiguous_tank | The response lists your tanks with their ids: use the id. |
| 400 | unknown_parameter · bad_unit · bad_value | The response says what that parameter accepts. |
| 400 | bad_measured_at | Unreadable date, in the future, or over a year old (usually an epoch in seconds read as milliseconds). |
| 429 | too_soon | Too early for that parameter: retryAfterSeconds says how long is left. |
| 429 | daily_cap | Past 500 requests for the day. The counter resets at midnight UTC. |
| 500 | server_error | Our fault: try again later. |
8 · Security and privacy
- The key is a bearer credential: whoever reads it can write to your tanks. Treat it like a password — no public repositories, no screenshots.
- Our server does not hold the key, it holds its fingerprint (sha256). We could not show it to you again even if we wanted to, which is why it has to be copied straight away.
- A key can only write readings: it cannot read your journal, touch your tasks or delete anything. The only thing it can read is the list of your tanks and the parameters they track (
GET /tanks), which is what it needs to know where to write. - You can revoke one whenever you like, or switch the toggle off to stop them all at once without losing the setup.
- In the app every reading says whether you entered it or it came from the API: looking at the history, you always know where a number came from.
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].