Frequently Asked Questions
Getting started with the Smart Connect API, or stuck on an error? This page collects the questions developers ask us most, with short answers and ready-to-copy examples. Click a question to see its answer. To share an answer, use the link icon next to the question.
Most asked
Browse by Topic
-
Access, the API add-on, supported devices and missing loggers.
-
Smart Connect, Data, Realtime and Device Management API compared.
-
Headers, base URL,
401and403errors, key expiry and security. -
The monthly request limit and how to stay within it.
-
Asynchronous requests, filters, date ranges, files and linking data.
-
Push and webhook options for live data.
-
What changes when you move from the Saveris 2 API.
Troubleshoot by Error
Most problems come with an error message. Find yours below and jump to the answer.
| You see | Most likely cause | Answer |
|---|---|---|
400 Bad Request |
Unknown field name, OData syntax or date range | Why do I get 400? |
401 "Authorization information is missing or invalid" |
Wrong header, extra characters in the key, expired key or wrong host | What should I check? |
403 "You have reached the maximum allowed number of API calls" |
Monthly limit used up | What happens when I reach the limit? |
403 "API key does not have permission for endpoint" |
Endpoint is not part of the Smart Connect API | What does 403 mean? |
403 from one network only |
Firewall or proxy on your side | What does 403 mean? |
| "Invalid request body" when creating a key | Key name has characters that are not allowed | Why does creating a key fail? |
"Incorrect username or password" on a /token endpoint |
Wrong API: Smart Connect uses an API key, not a login | Which API do I use? |
POST returns only a request_uuid, no data |
Requests are asynchronous | Why does my POST not return data? |
Status stays In Progress |
Large request | What if a request stays "In Progress"? |
| "API Key Management" missing in the menu | Add-on not booked, or your role | Why is API Key Management not shown? |
Getting Started
What can I do with the Smart Connect API?
The Smart Connect API gives you REST access to the data stored in your testo Smart Connect account:
- measurements
- alarms, including device and sensor system alarms
- device and sensor properties
- device status (battery, radio, connection)
- locations
- measuring objects
It is designed for retrieving stored data for reports, dashboards and integrations. See the Overview.
How do I get access to the Smart Connect API?
- Book the API add-on for your account in the Subscription Portal.
- Sign in to Smart Connect for your region, click your email address (top right) and choose API Key Management.
- Enter a key name, choose an expiry period and click + Generate. Copy the key right away, because it is shown only once.
You do not need to request an API user or password from Testo. Step-by-step instructions with screenshots: Generating an API Key.
"API Key Management" is not shown in my menu. Why?
Check the following:
- Is the add-on booked? The menu item only appears once the API add-on is active on your account.
- What is your role? Only users with the owner or admin role can manage API keys.
- Are you on the right portal? Make sure you are signed in at
www.<region>.smartconnect.testo.com.
Is API access included in my subscription? What does it cost?
API access is a paid add-on that you book in the Subscription Portal. It includes up to 10,000 API requests per month under the Fair Usage Policy.
Which devices are supported?
The API covers Stationary Monitoring (data loggers) only. Data from portable instruments and Smart Probes (for example testo 115i, 549i or 605i) is not available through the API.
Why are some of my loggers missing from the API?
The API only returns loggers that are registered in testo Smart Connect, either created there or migrated. Loggers that are still in the old Saveris cloud (saveris.net) are not included. Migrate them to Smart Connect first; see the Migration Guide.
Which testo API Is the Right One?
Smart Connect API, Saveris Data API, Realtime API or Device Management API: which one do I use?
These are separate products with separate hosts and logins. Using the credentials of one API with another is the most common cause of authentication errors.
| Smart Connect API | Saveris Data API | Realtime API (Push API) | Device Management API | |
|---|---|---|---|---|
| For | testo Smart Connect accounts | Saveris Food and partner integrations | Real-time stream over WebSocket | Onboarding and configuring devices |
| Host | data-api.<region>.smartconnect.testo.com |
data-api.<region>.p.savr.saveris.net |
tds-real-time-api.<region>.p.savr.saveris.net |
device-management-api.<region>.p.savr.saveris.net |
| Login | API key in x-custom-api-key |
API user, then POST /v1/token |
Token from the Data API | API user, then POST /token |
| Access | Self-service (API add-on) | Requested from Testo | Requested from Testo | Requested from Testo |
If you use testo Smart Connect, you only need the Smart Connect API and an API key. Your Smart Connect login does not work on any /token endpoint.
Is the Saveris Data API being discontinued? Do I have to switch?
No. The Saveris Data API is a separate product and is not discontinued. If you have a Data API account, keep using it as documented. Saveris 2 customers who move to Smart Connect switch to the Smart Connect API; see the Migration Guide.
Authentication & API Keys
How do I authenticate my requests?
Send your API key in the x-custom-api-key header with every request. There is no login or token call. Replace eu with your region (eu, am or ap).
curl -X POST "https://data-api.eu.smartconnect.testo.com/v3/devices/properties" \
-H "Content-Type: application/json" \
-H "x-custom-api-key: your-api-key-here" \
-d '{}'
import requests
BASE_URL = "https://data-api.eu.smartconnect.testo.com"
HEADERS = {"x-custom-api-key": "your-api-key-here"}
response = requests.post(f"{BASE_URL}/v3/devices/properties", headers=HEADERS, json={})
print(response.status_code, response.json())
// Node.js 18 or later, saved as a .mjs file
const BASE_URL = "https://data-api.eu.smartconnect.testo.com";
const HEADERS = {
"Content-Type": "application/json",
"x-custom-api-key": "your-api-key-here",
};
const response = await fetch(`${BASE_URL}/v3/devices/properties`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({}),
});
console.log(response.status, await response.json());
Do not put the key in an Authorization: Bearer or x-api-key header.
What is the correct base URL?
https://data-api.<region>.smartconnect.testo.com, where <region> is eu, am or ap. Your region is the two letters after www. in your Smart Connect address: for www.eu.smartconnect.testo.com, it is eu.
Do not add an environment part such as .i. or .p. to the host. Use an API key that was created in the same region you call.
I get 401 "Authorization information is missing or invalid". What should I check?
A 401 means the API could not match your key. Check, in this order:
- Header name: it must be exactly
x-custom-api-key. - Key value: no trailing line break, spaces, quotes or invisible characters. These are often added when the key is stored in an environment variable, a configuration file or a secrets manager.
- Expiry: the key has not expired or been revoked. You can check this in API Key Management.
- Host and region: use
data-api.<region>.smartconnect.testo.comwith the region the key was created in.
If the same key works on one machine or network but not on another, compare exactly what each environment sends. Proxies and scripts sometimes change header values.
I get 403 Forbidden. What does it mean?
- "You have reached the maximum allowed number of API calls (10000)": your monthly quota is used up. See Limits & Fair Usage.
- "API key does not have permission for endpoint: …": the endpoint is not part of the Smart Connect API. Only the endpoints in the API Reference are available.
- A
403from one network only: the API has no network-specific settings per customer. If your key works elsewhere, a firewall or proxy on your side is blocking or changing the request. Your IT team can check this, and we can help analyse concrete error messages, status codes and logs.
Creating an API key fails with "Invalid request body". Why?
The key name must be 3 to 99 characters long and may contain only letters, digits and the characters _, - and @. Spaces and other special characters are not allowed.
Do API keys expire?
Yes. You choose an expiry period when you create the key; the maximum is one year. Requests with an expired key are rejected. Create a replacement key before the old one expires, and switch your integration to it. Expired and revoked keys stay visible in API Key Management. See Key Expiration and Renewal.
How should I store and share my API key?
- Store it in a secrets manager or a protected configuration. Never put it in client-side code or public repositories.
- Use separate keys for development and production, rotate them regularly, and revoke keys you no longer use.
- Never send your API key to Testo support, by email or in a ticket. We never need it. If you shared a key by mistake, revoke it and create a new one.
Limits & Fair Usage
How many requests can I make?
10,000 API requests per month per account. Only POST requests count. The GET calls you use to poll for results are not counted. The counter covers the calendar month and resets on the first day of each month (UTC). See the Fair Usage Policy.
What happens when I reach the limit?
Further requests are rejected with 403 Forbidden:
{
"type": "ACCESS_DENIED",
"title": "User is not authorized to access this resource",
"detail": "You have reached the maximum allowed number of API calls (10000). Your usage counter will reset at the beginning of the next month in UTC.",
"status": 403
}
Access is restored automatically at the start of the next month. You don't need to do anything.
Can the limit be increased?
The limit covers regular integrations. Measurement data is processed in cycles of roughly 5 minutes, so requesting data more often returns nothing newer. Requesting new data every 5 minutes uses about 8,640 POST requests per month.
Instead of a higher limit, reduce the number of requests:
- Fetch all loggers in one request, not one request per logger.
- Request only the new time window since your last call. Don't download the last few days again on every call.
- Query device properties about once a day, because they rarely change.
- Use
$select, and increase the wait between status polls (5 s, 10 s, 20 s, …).
If your use case really needs more, contact your Testo sales partner. For real-time needs, see Real-Time Data.
Can my counter be reset?
The counter resets automatically every month. If you have a special situation, contact your Testo sales partner or support.
How can I see how many requests I have used?
There is currently no usage counter in Smart Connect. Keep track of your request count in your integration.
Retrieving Data
Why does my POST request not return any data?
All endpoints are asynchronous and work in the same three steps:
%%{init: {'theme': 'base', 'themeVariables': {'actorBkg': '#555555', 'actorBorder': '#e78d1f', 'actorTextColor': '#ffffff', 'actorLineColor': '#999999', 'signalColor': '#999999', 'signalTextColor': '#999999', 'labelBoxBkgColor': '#555555', 'labelBoxBorderColor': '#e78d1f', 'labelTextColor': '#ffffff', 'loopTextColor': '#999999'}}}%%
sequenceDiagram
participant You as Your integration
participant API as Smart Connect API
You->>API: 1. POST /v2/measurements
API-->>You: "Submitted" + request_uuid
loop Until "Completed"
You->>API: 2. GET /v2/measurements/{request_uuid}
API-->>You: "In Progress" … then "Completed" + data_urls
end
You->>API: 3. Download the files from data_urls
- Send a
POSTto the endpoint, for example/v2/measurements. You get back{"status": "Submitted", "request_uuid": "…"}. - Call
GETon the same endpoint with therequest_uuid, for example/v2/measurements/{request_uuid}, until the status isCompleted. Until then it isIn Progress. - Download the result files from
data_urls.
This applies to all six endpoints: /v3/devices/properties, /v4/devices/status, /v2/measurements, /v3/alarms, /v1/locations and /v1/measuring-objects.
The complete flow, ready to copy:
# 1. Submit the request
curl -X POST "https://data-api.eu.smartconnect.testo.com/v2/measurements" \
-H "Content-Type: application/json" \
-H "x-custom-api-key: your-api-key-here" \
-d '{"date_time_from": "2026-06-28T00:00:00Z", "date_time_until": "2026-06-29T00:00:00Z", "options": {"result_file_format": "CSV"}}'
# 2. Check the status with the request_uuid from step 1 (repeat until "Completed")
curl "https://data-api.eu.smartconnect.testo.com/v2/measurements/<request_uuid>" \
-H "x-custom-api-key: your-api-key-here"
# 3. Download a file from data_urls (no API key needed)
curl -o measurements.csv "<url from data_urls>"
import time
import requests
BASE_URL = "https://data-api.eu.smartconnect.testo.com"
HEADERS = {"x-custom-api-key": "your-api-key-here"}
# 1. Submit the request
submit = requests.post(
f"{BASE_URL}/v2/measurements",
headers=HEADERS,
json={
"date_time_from": "2026-06-28T00:00:00Z",
"date_time_until": "2026-06-29T00:00:00Z",
"options": {"result_file_format": "CSV"},
},
)
submit.raise_for_status()
request_uuid = submit.json()["request_uuid"]
# 2. Poll until the result is ready, waiting longer each time
wait = 5
for attempt in range(10):
time.sleep(wait)
status = requests.get(f"{BASE_URL}/v2/measurements/{request_uuid}", headers=HEADERS)
status.raise_for_status()
result = status.json()
if result["status"] == "Completed":
break
wait = min(wait * 2, 60)
else:
raise TimeoutError(f"Request {request_uuid} is still not completed")
# 3. Download the result files (no API key needed)
for number, url in enumerate(result["data_urls"]):
with open(f"measurements_{number}.csv", "wb") as file:
file.write(requests.get(url).content)
// Node.js 18 or later, saved as a .mjs file
import { writeFile } from "node:fs/promises";
const BASE_URL = "https://data-api.eu.smartconnect.testo.com";
const HEADERS = {
"Content-Type": "application/json",
"x-custom-api-key": "your-api-key-here",
};
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
// 1. Submit the request
const submit = await fetch(`${BASE_URL}/v2/measurements`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
date_time_from: "2026-06-28T00:00:00Z",
date_time_until: "2026-06-29T00:00:00Z",
options: { result_file_format: "CSV" },
}),
});
if (!submit.ok) throw new Error(`POST failed: ${submit.status}`);
const { request_uuid } = await submit.json();
// 2. Poll until the result is ready, waiting longer each time
let result;
let wait = 5;
for (let attempt = 0; attempt < 10; attempt++) {
await sleep(wait);
const status = await fetch(`${BASE_URL}/v2/measurements/${request_uuid}`, { headers: HEADERS });
if (!status.ok) throw new Error(`GET failed: ${status.status}`);
result = await status.json();
if (result.status === "Completed") break;
wait = Math.min(wait * 2, 60);
}
if (result.status !== "Completed") throw new Error(`Request ${request_uuid} is still not completed`);
// 3. Download the result files (no API key needed)
for (const [number, url] of result.data_urls.entries()) {
const file = await fetch(url);
await writeFile(`measurements_${number}.csv`, Buffer.from(await file.arrayBuffer()));
}
See Concepts for details.
Which fields can I filter and select on?
Each endpoint has its own set of fields. You find them in the API Reference, in the x-file-content section of each endpoint's GET response. Use exactly these names in $filter, $select and $orderby.
Field names differ between endpoints. For example, the serial number is called:
| Endpoint | Serial number field |
|---|---|
/v3/devices/properties |
device_serial_no and sensor_serial_no |
/v4/devices/status |
serial_no (of the device) |
/v2/measurements, /v3/alarms |
serial_no (of the sensor) |
A field name that does not exist for the endpoint causes a 400 Bad Request. Ready-to-use examples for each endpoint are in OData Filtering and Examples.
Which endpoints accept a date range?
Only /v2/measurements and /v3/alarms accept date_time_from and date_time_until:
- If you leave out
date_time_from, the API uses the last hour. - If you leave out
date_time_until, the API uses one hour afterdate_time_from.
The other endpoints return the current state: device properties, device status, locations and measuring objects. To find recent changes there, filter on the endpoint's time fields with $filter, for example device_last_modified for device properties or last_modified_at for locations.
How do I get only the most recent data?
-
Measurements and alarms: leave out
date_time_from, so that the API uses the last hour, and sort newest first. Then take the first row per sensor:{ "options": {"result_file_format": "CSV"}, "odata": {"$orderby": "timestamp desc"} }For alarms, sort by
alarm_time descinstead. -
Device properties, device status, locations and measuring objects: these endpoints always return the current state, so every request gives you the latest data.
Why do I get 400 Bad Request?
The most common causes, on every endpoint:
- Unknown field: a name in
$filter,$selector$orderbythat does not exist for this endpoint. For example, measurements have nodevice_uuidordevice_serial_no; usesensor_uuidorserial_nothere. See Which fields can I filter and select on? - OData syntax: put spaces around the operator and single quotes around text values, for example
serial_no eq '12345678'. - Date range (measurements and alarms only): use ISO timestamps in UTC, for example
2026-01-01T00:00:00Z, and make suredate_time_fromanddate_time_untilare not swapped.
Put the OData options in the JSON body under odata. A correct request for the measurements of one sensor:
curl -X POST "https://data-api.eu.smartconnect.testo.com/v2/measurements" \
-H "Content-Type: application/json" \
-H "x-custom-api-key: your-api-key-here" \
-d '{
"date_time_from": "2026-06-28T00:00:00Z",
"date_time_until": "2026-06-29T23:59:59Z",
"options": {"result_file_format": "CSV"},
"odata": {"$filter": "serial_no eq '\''12345678'\''"}
}'
import requests
response = requests.post(
"https://data-api.eu.smartconnect.testo.com/v2/measurements",
headers={"x-custom-api-key": "your-api-key-here"},
json={
"date_time_from": "2026-06-28T00:00:00Z",
"date_time_until": "2026-06-29T23:59:59Z",
"options": {"result_file_format": "CSV"},
"odata": {"$filter": "serial_no eq '12345678'"},
},
)
print(response.status_code, response.json())
// Node.js 18 or later, saved as a .mjs file
const response = await fetch("https://data-api.eu.smartconnect.testo.com/v2/measurements", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-custom-api-key": "your-api-key-here",
},
body: JSON.stringify({
date_time_from: "2026-06-28T00:00:00Z",
date_time_until: "2026-06-29T23:59:59Z",
options: { result_file_format: "CSV" },
odata: { $filter: "serial_no eq '12345678'" },
}),
});
console.log(response.status, await response.json());
See OData Filtering for all operators.
Can I request a very large time range?
We don't recommend it for measurements and alarms: very large time ranges take long to prepare and can fail. Split historical downloads into monthly or quarterly requests and combine the files on your side. For ongoing synchronisation, request only the period since your last successful call.
What if a request stays "In Progress" for a long time?
Large requests take longer to prepare. Keep polling the GET endpoint, but:
- Wait longer between polls each time, for example 5 s, then 10 s, then 20 s. Polls don't count against your monthly limit, but frequent polling adds unnecessary load.
- Set a timeout or a maximum number of polls, so your integration does not wait forever.
- Request less data, with a smaller time range or with
$selectto return only the fields you need.
If a request never completes, contact us with its request_uuid.
How long are download links valid, and can I reuse them?
The links in data_urls are valid for about one hour and work without an API key. Download the files soon after the status is Completed.
Within that hour you can download the same file again from the same link. That doesn't need another API request and doesn't count against your monthly limit.
Which file format should I choose?
Set it with "options": {"result_file_format": "…"}:
CSV(the default) for most cases. It opens in Excel and most tools.JSONfor processing in web applications.PARQUETorORCfor large volumes and analytics tools such as Pandas or Spark.AVROorTEXTFILEfor data pipelines that need them.
See File Formats.
Is the data real-time? How quickly is a new measurement available?
The Smart Connect API is for stored data, not live streaming. A measurement becomes available after the logger has sent it to the cloud and the cloud has processed it. How long that takes depends on the logger's communication interval; processing takes roughly 5 minutes. For real-time needs, see Real-Time Data.
Why do I see duplicate rows?
The API delivers measurements, alarms and device status at least once. When a logger sends data again to make sure it reaches the cloud, a record can appear twice. Remove duplicates using the uuid field of each record.
How do I link measurements, devices and locations?
The endpoints are linked through UUIDs:
- Device → sensor:
/v3/devices/propertiesreturns one row per channel, withdevice_uuidandsensor_uuid. Measurements and alarms identify the sensor (sensor_uuidandserial_no). - Sensor → measuring object: send
POST /v1/measuring-objectswith"$filter": "contains(channel_assignments, '<sensor_uuid>')", thenGETthe result. - Measuring object → location: read
back_reference:- Without an underscore (
_), it is the location UUID. - With an underscore, the part before it is the
equipment_uuidand the part after it is theequipment_parts_uuid.
- Without an underscore (
- Location: fetch
/v1/locationsand matchlocation_uuidwith theback_reference. Followparent_uuidto get the higher levels of the hierarchy.
Where are "Area" and "Group" from the old Saveris 2 API?
They are now part of the location hierarchy. Request the locations including the reference to the parent location:
{
"options": {"result_file_format": "CSV"},
"odata": {"$select": "location_uuid,name,location_type,location_subtype,parent_uuid"}
}
Start from the location in the measuring object's back_reference, as described in How do I link measurements, devices and locations? Then follow parent_uuid upwards through the hierarchy to find the levels that used to be the group and the area.
How do I get only the currently active alarms?
/v3/alarms returns active and resolved alarms for a time range. Add a filter to get only the active ones:
"odata": {"$filter": "alarm_status eq 'Alarm'"}
System warnings are included as alarms with alarm_type device system alarm or sensor system alarm.
Real-Time Data
Is there a push or webhook option?
The Smart Connect API has no push or webhook. Real-time streaming of measurements and alarms is provided by the separate Realtime API (Push API), which uses WebSocket. It is available for Smart Connect accounts only in exceptional cases, see Can I use the Realtime API with my Smart Connect account? For most monitoring scenarios, polling the Smart Connect API every 5 to 15 minutes is enough.
Can I use the Realtime API with my Smart Connect account?
Only in exceptional cases and on request. The Realtime API is not part of the Smart Connect API add-on. If your use case requires real-time data, contact support or your Testo sales partner and describe your requirements. If your request is approved, Testo activates the Realtime API for your account and sends you the connection details.
Migrating from Saveris 2
Do my Saveris 2 API access and call package carry over to Smart Connect?
No. After migrating, book the API add-on in the Subscription Portal and create a new API key. The endpoints, the authentication and the data format are different. The Migration Guide maps the old fields to the new ones.
Can I use the old and the new API at the same time during the migration?
No. Once your account and loggers are migrated, the old Saveris 2 API stops working for them. Don't register the same loggers or email address in a new Smart Connect account before migrating, because that can block the migration. Plan the switch so that your new integration is ready on migration day.
Can I test the Smart Connect API before migrating?
Not with your own data. Data from your loggers is only available through the Smart Connect API after your account has been migrated, and there is no other test option.
To avoid an interruption, Testo can issue your API key before the migration. Your integration can then switch over on migration day without a gap. Use the Migration Guide and the Examples to prepare your integration.
Still Stuck?
We're happy to help
If your question is not answered here, write to support.datasolutions@testo.de. To get an answer quickly, include:
- your region and your Smart Connect account (email address or account ID)
- the endpoint, the request body and the
request_uuid - the full response (status code and body) and the time in UTC
- the serial numbers of affected devices, if any
Never include your API key. We never need it to help you, so don't send it by email or in a ticket.