Device API documentation
How to send sensor readings to TankTutor. This is an optional feature, meant for people who already have probes connected.
Overview
If you have sensors — for example a TankTutor-PI or a third-party pH or conductivity meter — you can send measurements to your tank with a plain HTTP request. It is not needed to use TankTutor: you can always enter parameters by hand or through the chat.
Endpoint (the path is relative to the site origin): POST /api/devices/readings, with a JSON body.
Device keys
Every request is authenticated with a device key.
- Keys are created from the tank page (
/my-tanks/[id]), in the "Device keys" panel. An account is required. - At most 4 active keys per account, counting all tanks together.
- A key is bound to a single tank and cannot be moved.
- The full key (
ttk_…) is shown only once, at creation: afterwards only the first 8 characters stay visible. Store it right away. - Revoking a key frees its slot immediately. A revoked key gets 401.
Authentication
Send the key in the Authorization: Bearer ttk_YOUR_KEY header (the Bearer scheme is case-insensitive). No session cookie is needed.
Request format
The body is a JSON object with a readings array.
| Field | Type | Notes |
|---|---|---|
readings | array | Array of 1–3 items. A type cannot appear twice. |
type | string | One of temperature, ph, conductivity. |
value | number | Finite number. |
unit | string | Optional, temperature only: "C" or "F" (default "C"). The value is always stored in Celsius; the public badge shows °F only for accounts whose language is English. |
If even a single entry is invalid, the whole request is rejected with 400 and nothing is saved.
{
"readings": [
{ "type": "temperature", "value": 25.4, "unit": "C" },
{ "type": "ph", "value": 7.2 }
]
}Responses
| Status | Body | Meaning |
|---|---|---|
| 200 | {"saved": n} | Readings saved; n is the number of readings. |
| 400 | {"error":"invalid_device_readings"} | Invalid body (missing field, unknown or repeated type, non-numeric value, unsupported unit). Nothing is saved. |
| 401 | {"error":"authentication_required"} | Missing or malformed header. |
| 401 | {"error":"invalid_device_key"} | Unknown or revoked key. |
| 403 | {"error":"account_suspended"} | The account is suspended. |
| 429 | {"error":"rate_limited"} | You exceeded the rate limit. Nothing is saved. |
Limits and reliability
At most one request per time window is accepted per user, with all their keys counted together. The default is 5 minutes; the administrator can change it, so do not rely on a fixed value.
A 429 response saves nothing: retry after the window. The readings of one request are saved together, all or none.
Beta service: no SLA is guaranteed (availability and response times are not binding).
Examples
The addresses in the examples use the origin you are reading this page from.
curl -X POST https://tanktutor.it/api/devices/readings \
-H "Authorization: Bearer ttk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"readings":[{"type":"temperature","value":25.4,"unit":"C"},{"type":"ph","value":7.2}]}'import requests
resp = requests.post(
"https://tanktutor.it/api/devices/readings",
headers={"Authorization": "Bearer ttk_YOUR_KEY"},
json={
"readings": [
{"type": "temperature", "value": 25.4, "unit": "C"},
{"type": "ph", "value": 7.2},
]
},
timeout=10,
)
if resp.status_code == 200:
print("saved:", resp.json()["saved"])
elif resp.status_code == 429:
# Rate limited: retry after the window
print("rate limited")
else:
print("error:", resp.status_code, resp.json())