With the Turq API, your device writes to your tank's logbook on its own: readings from your sensor, doses from your dosing pump and completed tasks. It can send a push notification to your phone and fetch what it needs to work, such as a parameter's input limits or the latest reading.
log measurements, including several values from the same moment in one request
record a dose, for example when your dosing pump has run
mark the current occurrence of a task as done, for example when your auto feeder has fed
send a push notification to your own phone
fetch your tanks, parameters with unit and input limits, dosing products and task templates, optionally with target range, remaining amount and due status
fetch the latest reading of each parameter
check with a self-test whether your key works
What it can't do
It doesn't return history. You can only read the latest value of each parameter, no older readings, photos or journal entries.
It doesn't control equipment. Turq doesn't switch pumps, heaters or lights. Values you read aren't meant for your device to switch equipment with either, see Not for control.
It doesn't create anything. You set up tanks, task templates and dosing products in the app.
It isn't an alarm system. It only sends push notifications when your device triggers them, and whether they arrive also depends on your phone, network and push service. Don't rely on them alone for a critical failure.
You can use the API without a subscription, with a free, secured account. It only writes to tanks you can also edit in the app, and only reads tanks you can see in the app.
A secured Turq account. Guest accounts can't create keys. Securing your account is free and done in the app.
A tank you can see in the app. That can be your own or one someone shares with you. To write to it, you need to be able to edit it in the app; if it's read-only, it won't accept anything through the API, but you can still read it.
For tasks and doses, a task template or dosing product you created in the app beforehand.
A device or program that can send HTTPS requests with a custom header, such as Home Assistant, an ESPHome device, Node-RED or cURL.
For measurements, a correctly set clock, because every measurement carries a timestamp.
The rule behind it is simple: the API only writes where you can also write in the app, and only reads what you can see in the app. If you lose access to a shared tank, your key can neither write to it nor read it.
When new terms are published
When Turq publishes new terms or a new privacy policy, the app asks for your agreement the next time you open it. Your keys, however, keep working for a transition period, so your devices don't stop overnight. This applies if you had agreed to the previous version.
End of the period: While it runs, API responses carry the X-Turq-Consent-Deadline header. It gives the end of the period as an ISO 8601 timestamp in UTC. The app and the email about the new terms tell you the last day; the timestamp in the header is a little later, so that this day counts in full in every time zone.
After the period: If you haven't agreed by then, the API answers with 403 and CONSENT_REQUIRED. Your keys and data are kept. As soon as you agree in the app, your keys work again right away.
No extension: If another new version is published during the period, the period still ends at the stated time.
No period, no header: If the header is missing, you have agreed or no period is running. It only gives the end of the transition period, not necessarily the end of access: if your account keeps running longer for another reason, for example after you declined new terms, the header doesn't change.
Tip: Have your device read the header and show you a notice in good time, for example on your dashboard.
Your first entry in three steps
Create a key. In the app, open Settings › Integrations › API Keys and tap "Create API key". Give the key a name, such as "Home Assistant", and choose what it may do and which tanks it applies to. Copy the key right away, because Turq shows it only this once.
Test the connection. Send POST https://api.turq.app/v1/ping with the header Authorization: Bearer <your key>. If the API answers with "valid": true, the key works. The answer also lists what the key may do and the IDs of its tanks. The test doesn't log anything.
Test the connectionbash
curl -i -X POST "https://api.turq.app/v1/ping" \
-H "Authorization: Bearer <YOUR_API_KEY>"
Add the command. Right after creating the key, or later via "Set up device" on the key, the app builds the matching command with your IDs filled in, for example for Home Assistant or cURL. Paste it and replace the placeholders. After the first successful entry, Turq sends you a push notification that your device is connected.
Handling your key
A key is like a password for your tanks: whoever has it can log entries there in your name or read along, depending on what the key may do.
Keep it secret. Turq only stores a hash of the key. After creation nobody can display it again, not even us.
One key per device. Every push notification then shows which device it came from, and you can disconnect a single device without touching the others. If a device reads and writes, it still gets just one key with both permissions.
Only the permissions it needs. A temperature probe doesn't need push notifications and usually no read permissions either. You set permissions and tanks when you create the key; for different permissions, create a new key.
Only in a secret store. In Home Assistant and ESPHome, the key belongs in secrets.yaml; in Node-RED, in the node's credentials. Never in JavaScript that runs in the browser, such as a dashboard card: anyone who opens the dashboard can read it there.
Never in public repos, forums or screenshots. All keys start with turq_sk_, so you can search for that before you share a configuration.
You can revoke a key at any time under Settings › Integrations › API Keys › "Revoke key". It takes effect immediately. If a key got into the wrong hands, revoke it and create a new one.
What a key gives away if it gets into the wrong hands
With read permissions: the names of your tanks, dosing products and task templates, your parameters and, depending on the permissions, target ranges, remaining amounts, due tasks and the latest readings. Always only for the key's tanks.
With write permissions, someone can log entries in your name and, if the key may do that, send you push notifications. If the key can also read, a stranger finds the IDs needed for writing more easily, and their entries stand out less.
No key reveals your sign-in details, your email address or tanks that aren't linked to it.
The app shows how many keys you can have active at once.
Reading values and settings
Your device can fetch what it needs to work. There are three read permissions for this. None of them includes another, so you choose exactly what a key may see. When you create a key, you'll find them in the app under "Can read"; nothing is preselected there.
CONFIG_READ, "Core data" in the app: the key's tanks, the parameters with unit, decimal places, step size and plausible range, the dosing products and the task templates, each with their IDs. This way your device knows the allowed input before it sends a value.
STATUS_READ, "Current status" in the app: adds the current state to these responses: the target range of each parameter, the remaining amount of each dosing product, and whether a task is due right now and when it's due next. On its own it does nothing, so in the app you choose it together with "Core data".
MEASUREMENTS_READ, "Latest measurements" in the app: the newest value of each parameter, with its time and origin. The API doesn't return older values.
Read and write permissions can be combined on one key. A device that reads and logs entries needs just one key.
Older keys can only write. A key's permissions can't be changed, and keys from before version 1.1 don't get read permissions added. To read, create a new key and revoke the old one once your device has moved over.
Read-only tanks stay readable. Only entries are rejected there, with AQUARIUM_LOCKED. In the list of tanks, readOnly shows which ones they are.
Shared tanks: you can read them as long as you have access in the app. Members of a tank you share can also create keys for their devices to read it. You see these devices in the app on the tank under "Members’ devices" and can disconnect them there.
Example: a dial next to the tank
A small dial with a display hangs next to the tank, for example one based on an ESP32. You measure KH with a drop test, turn the dial to the value and press. For this it needs a single key with CONFIG_READ, MEASUREMENTS_READ and MEASUREMENTS_WRITE:
At startup it calls GET /v1/aquariums/<tank ID>/parameters?parameters=kh and gets the unit, step size, decimal places and plausible range. That way the dial only offers values Turq will accept.
Then it fetches the latest value with GET …/measurements/latest?parameters=kh and shows it together with its age.
After measuring it sends the dialed value with POST …/measurements and an Idempotency-Key. That way a repeat after a dropped connection still creates only one entry.
If the key also has TASKS_WRITE, the same dial marks the task "Measure KH" as done right away. With STATUS_READ it shows beforehand whether that task is due.
How often your device should ask
Structure (tanks, parameters, dosing products, task templates): at startup and then about once an hour. It rarely changes.
Latest value: at most every 5 minutes. Asking more often doesn't make it fresher, because it only changes when someone measures and logs a value.
Conditional requests save data: send the ETag of the last response as If-None-Match. If nothing has changed, you get 304 without a body. For the limits, though, a 304 counts like any other request: it saves data, not requests.
On 429, wait the seconds given in Retry-After, then continue at your usual pace.
Headers for reading
ETag (response): identifies the content of the response. Keep it together with the response and send it back unchanged, including a leading W/.
If-None-Match (request): the ETag you kept. If the API answers with 304, the response you kept is still valid.
Cache-Control: private, no-store (response): proxies and browsers don't store read responses. Your device may keep them itself.
Date (response): the server time. Work out the age of a value from timestamp and this header, not from your device's clock.
X-Turq-Consent-Deadline (response): only set during a transition period after new terms.
When reading doesn't work
SCOPE_DENIED with an older key: it can only write. Create a new key with read permissions.
SCOPE_DENIED despite a read permission: each route needs its own permission. …/measurements/latest needs MEASUREMENTS_READ, the other read routes need CONFIG_READ.
SERVICE_DISABLED only when reading: we've paused reading for now, but the API still accepts entries. Show the last state you read together with its age and ask again later.
CONSENT_REQUIRED: the transition period after new terms has ended. As soon as you agree in the app, the API answers again right away.
AQUARIUM_NOT_ALLOWED: the tank no longer exists, you no longer have access to it, or it isn't linked to the key. GET /v1/aquariums and the self-test list the tanks your key can use right now.
304 without a body: not an error. Nothing has changed since your last request. If your device didn't keep anything, for example after a restart, ask without If-None-Match.
A parameter is missing from the response: there's no value yet, the parameter isn't active in Turq, or its parameter key is misspelled. The API skips unknown parameter keys instead of reporting an error.
value is null or operator is set: the value was logged as a limit, such as "less than" or "below the detection limit" (belowDetection). Show that too, not just a number.
VALIDATION_FAILED with ?parameters=: send the parameter keys as one comma-separated list, such as parameters=kh,ca, not as parameters[]=.
Not for controlling devices
Values you read are meant for display: on a screen next to the tank, on your dashboard, in a reminder. Turq doesn't support using them to control equipment, such as switching a dosing pump, a heater or an auto top-off based on a Turq value. That's why you won't find recipes for that here.
The reason: a value in Turq is a logbook entry, not a real-time measurement.
It can be wrong. A typo, an old reagent or a drifting sensor ends up in the logbook like any other value.
It can be backdated.timestamp is the time given by whoever logged the value, not when it reached Turq.
It can come from someone else. Members of a shared tank and their devices log entries to the same logbook.
It can be missing. The API may be paused, a limit may be reached, or the connection may be down. We don't commit to any availability.
To help you judge a value, it carries origin (how it got into the logbook, for example via the app, a lab import or the API), viaApi (whether a device logged it with a key) and timestamp. Always show a value's age along with it.
In short
If a wrong or missing value could harm your animals, it doesn't belong in a control loop. Control your equipment with sensors connected directly to your controller, and use Turq as the logbook alongside it.
Display names as text only
The names of tanks, dosing products and task templates were typed by people, in shared tanks also by members. Turq removes control characters and shortens long names, but doesn't check what they say.
Only display them as text. Never insert them unchecked into HTML, code, templates or commands.
Don't pass them to voice assistants or chatbots that may switch devices. A name can be phrased like an instruction.
Work with IDs, not names. Names can change at any time and don't have to be unique.
Recipes
Here you can see what the request looks like for common systems. For Home Assistant and cURL, the app builds the command with your IDs; the examples here show what's inside. We name these systems only to help you find the right recipe. Turq is not affiliated with any of these projects.
What you replace in every recipe
the key placeholder with your key (turq_sk_…)
the tank ID with your tank's ID. It's in the command from the app and in the self-test's answer.
for tasks, the task template ID; for doses, the dosing product ID. Both are in the command from the app; a key with CONFIG_READ can also fetch them itself.
for measurements, the parameter key, such as ph. The app shows it during setup under "Send a measurement", and CONFIG_READ returns it too.
Send numbers as numbers with a dot, so 7.4 rather than 7,4, and not as text in quotes.
Home Assistant sends the request with its built-in REST command (rest_command). There is no separate Turq integration, and you don't need one. For measurements, an automation reads your sensor on a fixed schedule and calls the command. Replace the placeholder sensor with your sensor's entity and put the key into secrets.yaml as a !secret instead of writing it into configuration.yaml.
If your ESPHome device already reports to Home Assistant, the Home Assistant recipe is the better choice, because then the key lives in one place only. To send directly from the device, use the http_request component and the http_request.post action. An ESP32 verifies the certificate of api.turq.app; ESPHome can't do that on an ESP8266, so send through Home Assistant from there. The device needs the time, for example via the time component with SNTP, and here too the key belongs in secrets.yaml. Important: send the reading as a number, not as text.
In Node-RED, a single http request node with the POST method is enough. Turn on authentication there, choose bearer and enter your key. Node-RED then keeps it with its credentials, and it won't end up in an exported flow. Build the request content beforehand in a function or change node as an object in msg.payload.
Method: POST
URL: the address of the route, for example https://api.turq.app/v1/aquariums/3f9a6b52-1c1e-4d57-8a3a-6f2d3a8b9c01/measurements
Authentication: bearer, with your key
Return: a parsed JSON object
Send a measurementPOST /v1/aquariums/{aquariumId}/measurementsjs
With cURL you can try every route straight from the terminal. Add -i to see the response headers too, such as what's left for today. For a fixed schedule, add the line to your crontab; there, the percent sign has to be written as \%. To keep the key out of your shell history, put it in an environment variable.
Set the key for this sessionbash
read -rs TURQ_API_KEY && export TURQ_API_KEY
Test the connectionbash
curl -i -X POST "https://api.turq.app/v1/ping" \
-H "Authorization: Bearer $TURQ_API_KEY"
To keep the API reliable for everyone, Turq limits how often data is read and how often and how much is written. The limits are the same for everyone, with or without Pro. We deliberately don't list fixed numbers here, because we adjust them as we go. Your device learns everything it needs from the responses.
Too many requests at once: the API answers with 429 and RATE_LIMITED. The Retry-After header gives the seconds to wait.
Daily limit per tank: measurements and doses also have a limit per tank and day. Successful responses carry the headers X-Turq-Daily-Limit (the limit) and X-Turq-Daily-Remaining (what's left today). For measurements, every single value counts, not every request. The day starts at 00:00 UTC.
Limit reached: you get 429 with DAILY_LIMIT_REACHED, and Retry-After counts down to midnight UTC. The app also shows a notice in your notifications.
Rejected requests and repeats with the same Idempotency-Key don't count.
Reading has its own limits, separate from writing. There too, too many requests return 429 with RATE_LIMITED and Retry-After. Reading has no daily limit per tank, and a 304 counts like any other request.
How to stay comfortably below them
Send measurements on a fixed schedule instead of on every small change. The commands from the app send every 15 minutes; water parameters change slowly.
Bundle the values from one moment into one request, such as temperature and pH together.
Don't retry failed requests in a tight loop. Follow Retry-After, and for errors without that header, wait a little longer each time.
Send push notifications only for events, not on a schedule.
Only fetch values as often as your device needs them. You'll find the right pace under Reading.
Avoiding duplicate entries
Give write requests an Idempotency-Key header with a unique value per event, such as a UUID, and reuse that value when you repeat the same request: Turq then logs the event only once and answers the repeat with "replayed": true. If the first request is still running, you get 409 with IDEMPOTENCY_IN_PROGRESS and Retry-After, and the same value with different content returns 409 with IDEMPOTENCY_KEY_REUSED.
Errors
Every error has the same format: {"error": {"code": …, "message": …, "requestId": …}}. Have your code check code, not the text in message, which may change. We need the requestId if you contact us.
Code
Status
Meaning
What helps
UNAUTHORIZED
401
The key is missing, mistyped or revoked.
Check the Authorization: Bearer <key> header; replace revoked keys with a new one.
ACCOUNT_NOT_SECURED
403
The account behind the key is a guest account.
Secure the account in the app.
CONSENT_REQUIRED
403
The account behind the key hasn't agreed to the current terms or privacy policy, and there is no transition period or it has ended.
Open the app and agree. The keys then work again right away.
API_KEY_SUSPENDED
403
We have suspended the key or API access for this account.
Ask us in the app under Help & Feedback.
SCOPE_DENIED
403
The key lacks the permission for this route, for example an older key trying to read.
Create a new key with the right permission; a key's permissions can't be changed.
AQUARIUM_NOT_ALLOWED
403
The key isn't linked to this tank, the tank no longer exists, or you no longer have access to it.
Check the tank ID; the self-test and GET /v1/aquariums list your key's tanks.
AQUARIUM_LOCKED
403
The tank is read-only in the app. Reading still works; it just won't accept entries.
The app shows on the key how the tank can accept data again. For shared tanks, it depends on the account that owns the tank.
VALIDATION_FAILED
400
The request doesn't match the reference, for example text instead of a number, an unknown parameter, an implausible timestamp or, when reading, a malformed parameters list.
message gives a hint; compare your request with the reference.
NOT_FOUND
404
Something in the request, such as the task template or dosing product, doesn't exist in this tank.
Use the ID from the app's command or fetch it with CONFIG_READ.
NO_DUE_TASK
404
Nothing is due for this task right now, or the current occurrence is already done, for example because someone else fed the fish today. The API does not catch up older open occurrences.
Nothing. This is a normal result.
IDEMPOTENCY_IN_PROGRESS
409
A request with the same Idempotency-Key is still running.
Retry after Retry-After.
IDEMPOTENCY_KEY_REUSED
409
The same Idempotency-Key was already used with different content.
Use a new value for every new event.
CONFLICT
409
The request doesn't match the current state, for example because the same task was marked done at the same time.
Wait briefly and try again.
RATE_LIMITED
429
Too many requests in a short time.
Wait for Retry-After and send less often.
DAILY_LIMIT_REACHED
429
This tank's daily limit is reached.
It continues at 00:00 UTC. Send less often or fewer values.
SERVICE_DISABLED
503
The API, or just reading, is paused right now. If only reading is paused, the API still accepts entries. Your keys are kept.
Try again later.
INTERNAL_ERROR
500
Something went wrong on our side, or the request couldn't be processed (then with a status below 500).
Try again later. If it persists, contact us with the requestId.
Reference
All routes, fields and responses in detail. The base URL is https://api.turq.app/v1; every request needs the Authorization: Bearer <key> header, and write requests send JSON. The reference is generated from the same file you can download here. We check it against the code automatically with every change.
OpenAPI 3.1. Import the file into your API tool or generate a client from it.
POST/v1/ping
Check an API key
Scope: none
Verifies that the key is valid and active and returns the scopes and the IDs of the aquariums the key can use right now. Needs no scope and writes nothing. The request needs no body. This route has its own, stricter request limit.
Responses
Status
Meaning
200
The key is valid. Headers: X-Turq-Consent-Deadline
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED (the owning account is a guest account), CONSENT_REQUIRED (the owning account has not accepted the current terms and the transition period after the new version has ended), API_KEY_SUSPENDED (the key or its account is suspended).
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
Records one measurement with one or more parameter values at a point in time. Required scope: MEASUREMENTS_WRITE.
Parameter keys are the Turq parameter keys, for example ph, kh, gh, temperature, no3, po4, salinity. Values are plain numbers in the unit Turq uses for that parameter. Values outside the plausible range of a parameter are rejected.
Water values change slowly. A fixed interval of several minutes is usually enough; do not send on every sensor reading.
The daily allowance is counted in individual values, not in requests.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
Idempotency-Key optional
header
string
Optional unique value (for example a UUID) chosen by the client. A repeat of the same request with the same key and body does not write again.
Request body (JSON)
Field
Type
Description
timestamp required
integer
Time of the measurement as Unix time in milliseconds. Not earlier than 2000-01-01 and not noticeably in the future.
values required
object (values: number)
Parameter key to numeric value. Keys must be non-empty and short, the number of entries is limited, and values must be finite numbers. max 200 entries; keys 1 to 50 characters
The measurement was recorded. Headers: X-Turq-Daily-Limit, X-Turq-Daily-Remaining, X-Turq-Consent-Deadline
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it), AQUARIUM_LOCKED (the aquarium is read-only).
409
IDEMPOTENCY_KEY_REUSED (the key was already used with a different body), IDEMPOTENCY_IN_PROGRESS (the first request with this key is still running; repeat after Retry-After) or CONFLICT. Headers: Retry-After
429
RATE_LIMITED (too many requests) or DAILY_LIMIT_REACHED (the daily allowance of this aquarium is used up). Wait Retry-After seconds. For the daily allowance the wait ends at the start of the next UTC day. Headers: Retry-After, X-Turq-Daily-Limit, X-Turq-Daily-Remaining
500
Unexpected server error.
503
The API is temporarily switched off.
Response 201json
{
"id": "0b7e2c5a-6f1d-4b8e-9a3c-2d4f6a8c0e12"
}
POST/v1/aquariums/{aquariumId}/dosing-events
Record a dosing event
Scope: DOSING_WRITE
Records that a dosing product was dosed (or refilled or calibrated) in the aquarium. Required scope: DOSING_WRITE.
The product must belong to the aquarium in the path. The type value manual_dose records a dose and reduces the remaining volume of the product by amount; refill and calibration are statements about the remaining volume. If timestamp is omitted the server time is used.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
Idempotency-Key optional
header
string
Optional unique value (for example a UUID) chosen by the client. A repeat of the same request with the same key and body does not write again.
Request body (JSON)
Field
Type
Description
productId required
string (uuid)
Dosing product of the aquarium in the path.
amount required
number
Amount in the unit of the product.
type required
string
Kind of event. manual_dose, refill and calibration change the remaining volume of the product.
unit optional
string
Unit label, for example ml. max length 10
notes optional
string
max length 500
timestamp optional
number
Unix time in milliseconds. Defaults to the server time.
The dosing event was recorded. Headers: X-Turq-Daily-Limit, X-Turq-Daily-Remaining, X-Turq-Consent-Deadline
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it), AQUARIUM_LOCKED (the aquarium is read-only).
404
A referenced resource, for example the dosing product, was not found in this aquarium.
409
IDEMPOTENCY_KEY_REUSED (the key was already used with a different body), IDEMPOTENCY_IN_PROGRESS (the first request with this key is still running; repeat after Retry-After) or CONFLICT. Headers: Retry-After
429
RATE_LIMITED (too many requests) or DAILY_LIMIT_REACHED (the daily allowance of this aquarium is used up). Wait Retry-After seconds. For the daily allowance the wait ends at the start of the next UTC day. Headers: Retry-After, X-Turq-Daily-Limit, X-Turq-Daily-Remaining
500
Unexpected server error.
503
The API is temporarily switched off.
Response 201json
{
"id": "5d3c1a9e-8b7f-4e6d-a2c4-0f1e2d3c4b5a"
}
POST/v1/aquariums/{aquariumId}/tasks/complete
Mark the current task occurrence as done
Scope: TASKS_WRITE
Marks the current occurrence of a recurring task as done: today's latest occurrence that is already due, otherwise today's next one, otherwise the most recent overdue one. Older open occurrences stay open; they are never worked off by this route (the Turq app lets people mark a backlog as missed). Required scope: TASKS_WRITE.
The task template must belong to the aquarium in the path. If the task has no occurrence that can be completed right now, the API answers with the error code NO_DUE_TASK. This includes a repeated call: once the current occurrence is done, a second call does not complete an older one but answers NO_DUE_TASK. The same holds for two calls at the same time: one completes the occurrence, the other gets NO_DUE_TASK.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
Idempotency-Key optional
header
string
Optional unique value (for example a UUID) chosen by the client. A repeat of the same request with the same key and body does not write again.
Request body (JSON)
Field
Type
Description
templateId required
string (uuid)
Task template of the aquarium in the path.
note optional
string
max length 500
metadata optional
object | null
Free-form JSON object that is stored with the completion. Limited in size.
{
"templateId": "7c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f",
"note": "Water change done by the automation"
}
Responses
Status
Meaning
201
The task occurrence was marked as done. Headers: X-Turq-Consent-Deadline
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it), AQUARIUM_LOCKED (the aquarium is read-only).
404
NOT_FOUND (the task template was not found in this aquarium) or NO_DUE_TASK (there is no occurrence that can be completed right now).
409
IDEMPOTENCY_KEY_REUSED (the key was already used with a different body), IDEMPOTENCY_IN_PROGRESS (the first request with this key is still running; repeat after Retry-After) or CONFLICT. Headers: Retry-After
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
500
Unexpected server error.
503
The API is temporarily switched off.
Response 201json
{
"id": "c4a1e8b2-3d5f-4a7c-9e0b-6f8d1a2b3c4d"
}
POST/v1/aquariums/{aquariumId}/notifications
Send a push notification to your own devices
Scope: NOTIFICATIONS_WRITE
Sends a push notification to the devices of the account that owns the key. Other members of the aquarium do not receive it. Required scope: NOTIFICATIONS_WRITE.
The title of the push notification is always set by Turq to "🔌 <key name> · <aquarium name>", so the message is recognizable as coming from an integration. A device cannot change it. The title and body you send appear in the message text below that heading, title on the first line and body on the next. Both are plain text; control characters are removed. This route has its own, stricter request limits.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
Idempotency-Key optional
header
string
Optional unique value (for example a UUID) chosen by the client. A repeat of the same request with the same key and body does not write again.
Request body (JSON)
Field
Type
Description
title required
string
First line of the message text. It is not the title of the push notification. max length 80
body required
string
Second line of the message text. max length 240
Example: A warningjson
{
"title": "Tank",
"body": "Water level is low"
}
Responses
Status
Meaning
200
The notification was accepted for delivery. Headers: X-Turq-Consent-Deadline
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it), AQUARIUM_LOCKED (the aquarium is read-only).
409
IDEMPOTENCY_KEY_REUSED (the key was already used with a different body), IDEMPOTENCY_IN_PROGRESS (the first request with this key is still running; repeat after Retry-After) or CONFLICT. Headers: Retry-After
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
500
Unexpected server error.
503
The API is temporarily switched off.
Response 200json
{
"sent": true
}
GET/v1/aquariums
List the aquariums of a key
Scope: CONFIG_READ
Returns the aquariums the key is bound to and the account can currently use. Required scope: CONFIG_READ. readOnly is true when the aquarium is read-only in the Turq app: it can be read, but writes are rejected. This route is not counted as activity of an aquarium.
Parameters
Name
In
Type
Description
If-None-Match optional
header
string
The ETag of an earlier response. When nothing changed, the API answers 304 Not Modified without a body.
Responses
Status
Meaning
200
The aquariums of the key. Headers: ETag, X-Turq-Consent-Deadline
304
Nothing changed since the response with the ETag you sent in If-None-Match. No body. The 304 still counts against the request limits. Headers: ETag
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the read scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it). A read-only aquarium is not an error here.
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
Returns the water parameters a device can enter, with the unit, the input step and the plausible range. Use them to limit input on the device before you send a value. Required scope: CONFIG_READ.
unit is the unit the API expects and returns for the parameter (a plain ASCII label, or null). decimals is the number of decimal places of step. plausibleMin and plausibleMax are hard limits; null means that no limit is stored, not that every value is valid. With the scope STATUS_READ each parameter also has target, the target range of this aquarium (null when none is set).
The optional query parameters limits the list to a comma separated list of parameter keys. Unknown or inactive keys are ignored. A malformed list is rejected with VALIDATION_FAILED.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
parameters optional
query
string
Comma separated parameter keys, for example kh,ca. One single value: the array form parameters[]=kh and a repeated parameters are rejected. Keys use lower-case letters, digits and underscores only. Unknown or inactive keys are ignored.
If-None-Match optional
header
string
The ETag of an earlier response. When nothing changed, the API answers 304 Not Modified without a body.
Responses
Status
Meaning
200
The parameters. Headers: ETag, X-Turq-Consent-Deadline
304
Nothing changed since the response with the ETag you sent in If-None-Match. No body. The 304 still counts against the request limits. Headers: ETag
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the read scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it). A read-only aquarium is not an error here.
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
Returns the dosing products with the IDs you need for the dosing route. Required scope: CONFIG_READ. With the scope STATUS_READ each product also has remainingVolume, the remaining volume as the app calculates it (null when unknown). It changes only a few times a day.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
If-None-Match optional
header
string
The ETag of an earlier response. When nothing changed, the API answers 304 Not Modified without a body.
Responses
Status
Meaning
200
The dosing products. Headers: ETag, X-Turq-Consent-Deadline
304
Nothing changed since the response with the ETag you sent in If-None-Match. No body. The 304 still counts against the request limits. Headers: ETag
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the read scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it). A read-only aquarium is not an error here.
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
Returns the active recurring tasks with the IDs you need for the task route. Required scope: CONFIG_READ. With the scope STATUS_READ each template also has dueNow (true when the task route can complete an occurrence right now) and nextDueAt (Unix time in milliseconds of the occurrence the task route would complete now, otherwise of the next one that is not done yet, starting tomorrow, or null).
When many templates have to be calculated at once, the server limits the work per request. The two status properties can then be missing for some templates. A missing property means unknown, not false: ask again later or use the task route, which answers NO_DUE_TASK when nothing is due.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
If-None-Match optional
header
string
The ETag of an earlier response. When nothing changed, the API answers 304 Not Modified without a body.
Responses
Status
Meaning
200
The task templates. Headers: ETag, X-Turq-Consent-Deadline
304
Nothing changed since the response with the ETag you sent in If-None-Match. No body. The 304 still counts against the request limits. Headers: ETag
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the read scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it). A read-only aquarium is not an error here.
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
Returns, for each parameter that has a value in the aquarium, the most recent one, as a map from parameter key to value. A parameter without a value is missing from the map. Required scope: MEASUREMENTS_READ.
value is null when the entry is a bound such as "below 0.05"; then operator is < or >, and belowDetection is true when the value was below the detection limit. timestamp is the time the writer gave for the measurement (Unix time in milliseconds), not the time of arrival. origin tells where the value came from: app (entered in the app), lab (lab report), api (an integration), assistant (the Qualy assistant) or other. viaApi is true when it was written with an API key.
The optional query parameters limits the map to a comma separated list of parameter keys. Unknown or inactive keys are ignored. A malformed list is rejected with VALIDATION_FAILED. The latest value changes only when someone measures; do not ask for it more often than your use needs.
Parameters
Name
In
Type
Description
aquariumId required
path
string (uuid)
ID of the aquarium. The key must be bound to it.
parameters optional
query
string
Comma separated parameter keys, for example kh,ca. One single value: the array form parameters[]=kh and a repeated parameters are rejected. Keys use lower-case letters, digits and underscores only. Unknown or inactive keys are ignored.
If-None-Match optional
header
string
The ETag of an earlier response. When nothing changed, the API answers 304 Not Modified without a body.
Responses
Status
Meaning
200
The latest values. Headers: ETag, X-Turq-Consent-Deadline
304
Nothing changed since the response with the ETag you sent in If-None-Match. No body. The 304 still counts against the request limits. Headers: ETag
400
The path, the query or the body is invalid. The message lists what is wrong.
401
The key is missing, malformed, unknown or revoked.
403
The request is not allowed. Possible codes: ACCOUNT_NOT_SECURED, CONSENT_REQUIRED (after the transition period), API_KEY_SUSPENDED, SCOPE_DENIED (the key lacks the read scope of this route), AQUARIUM_NOT_ALLOWED (the key is not bound to this aquarium, or the account may no longer use it). A read-only aquarium is not an error here.
429
Too many requests. Wait Retry-After seconds before you retry. Headers: Retry-After
This is where we note what changes in the API, newest first. The major version is part of the path (/v1). New fields, values and routes are added within /v1, so your code should ignore fields and values it doesn't know. We try to avoid changes that would require you to adjust running devices and list them here once they're settled. Turq is a small project: we don't commit to any availability or minimum support period for a version, and we may pause the API temporarily.
1 November 2026 · Tasks: the current occurrence
Changed: POST …/tasks/complete now marks the current occurrence of the task as done: today's latest occurrence that is already due, otherwise today's next one, otherwise the most recent overdue one. Before, it was the oldest open occurrence.
Changed: older open occurrences stay open and are never worked off. A repeated call does not complete an older occurrence but answers NO_DUE_TASK. This also holds for two calls at the same time: one completes it, the other gets NO_DUE_TASK.
Changed: dueNow and nextDueAt follow the same occurrence. dueNow is true when completing works right now, and nextDueAt then names exactly that occurrence. Otherwise it names the next occurrence from tomorrow on that is not closed yet.
For your device: if it completes once per event, nothing changes. If it used to complete several times after an outage to catch up on missed occurrences, it no longer does. You complete such occurrences in the app or mark them there as missed.
1 November 2026 · Version 1.1: reading
New: five read routes, GET /v1/aquariums and, per tank, …/parameters, …/dosing-products, …/task-templates and …/measurements/latest.
New: the permissions CONFIG_READ, STATUS_READ and MEASUREMENTS_READ. None includes another, and read and write permissions can be combined on one key. Existing keys can still only write.
New: ETag and If-None-Match with the 304 response; read responses carry Cache-Control: private, no-store.
Changed: the self-test now only lists tanks the key can use right now, and scopes may contain read permissions.
Changed: SERVICE_DISABLED can also apply to reading only; the API then still accepts entries.
Unchanged: write routes, the limits for writing and the error codes. Values you read are meant for display, not for controlling devices.
1 November 2026 · Open to every secured account
The API can now be used without a subscription, with a secured account. It only writes to tanks you can edit in the app.
New: a daily limit per tank for measurements and doses, with the headers X-Turq-Daily-Limit and X-Turq-Daily-Remaining and the code DAILY_LIMIT_REACHED.
New: the codes ACCOUNT_NOT_SECURED, CONSENT_REQUIRED, API_KEY_SUSPENDED, AQUARIUM_LOCKED and IDEMPOTENCY_IN_PROGRESS.
New: after new terms or a new privacy policy, keys keep working for a transition period. The X-Turq-Consent-Deadline header gives its end; after that you get CONSENT_REQUIRED until you agree in the app.
Changed: read-only tanks answer with AQUARIUM_LOCKED instead of SCOPE_DENIED. Push notifications from such tanks are now rejected too. Tanks you no longer have access to answer with AQUARIUM_NOT_ALLOWED instead of 404 or NO_DUE_TASK.
PRO_REQUIRED is no longer used.
Help and feedback
Send questions, bugs or ideas about the API straight from the app: Settings › Help & Feedback. That's also where you'll find our reply. For errors, include the code, the requestId and what your device sent. We never need your key, so please don't include it. And if you're missing a route, tell us what you'd use it for.