SensorThings API Extractor avatar

SensorThings API Extractor

Pricing

from $0.35 / 1,000 records

Go to Apify Store
SensorThings API Extractor

SensorThings API Extractor

Point at ANY OGC SensorThings API v1.1 instance (FROST-Server, GOST, SensorUp and thousands of air-quality, smart-city and research deployments) and get flat sensor rows — observations, things with coordinates, datastreams with units — plus raw JSON. @iot.nextLink paging, OData filters. Pay per row.

Pricing

from $0.35 / 1,000 records

Rating

0.0

(0)

Developer

Datamule

Datamule

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Point at any OGC SensorThings API v1.1 instance and get clean, flat sensor rows — plus the lossless raw JSON of every entity. One actor, the entire open sensor-data world.

The OGC SensorThings API (OGC 15-078r6 / 18-088) is the open standard for Internet-of-Things and environmental-sensor data on the web. Every conforming deployment — no matter what server it runs (FROST-Server, GOST, SensorUp, or a custom stack) — answers the identical RESTful JSON grammar over the same entity graph, and anonymous read is the norm (publishing sensor data is the protocol's purpose). So this single actor works against every SensorThings instance on Earth — air-quality networks, smart-city platforms, government monitoring, university research. No per-instance scraper, no host enumeration — just the base URL.

What you can extract

Pick a mode and point sources at one or more SensorThings base URLs. A plain server address such as https://iot.hamburg.de also works: the actor finds its /v1.1 (or /v1.0) root and logs which one it used.

ModeWhat you get (one row per…)
observations (default)sensor reading — @iot.id, phenomenon time, result time, result (any type), result quality, parameters, valid time, the Datastream it belongs to (datastreamName, datastreamId) and its unit (unitName, unitSymbol) + Datastream / FeatureOfInterest links
thingsmonitored asset — id, name, description, properties, and its Location's coordinates (Locations auto-expanded)
datastreamsmeasurement series — name, observation type, unit of measurement, observed area, phenomenon/result time + Sensor / ObservedProperty / Thing links
discoveryinstance fingerprint — available entity sets, declared conformance classes, server settings

The SensorThings entity graph is

Things → Locations → Datastreams → (Sensor, ObservedProperty) → Observations
. Run discovery first to fingerprint an instance, things or datastreams to map what it monitors, then observations (optionally scoped to a single datastreamId) to pull the readings.

Sensor readings spreadsheet (CSV) without code

Fastest start: the Console form opens pre-filled with one live sensor: Air temperature at DE-Bnb-ECStation (datastream 723, unit °C, updated every few minutes) on the KIT TERENO environmental server (https://sensorthings.imk-ifu.kit.edu/v1.1), newest first. Click Start to get its newest readings. The pre-filled Max records of 5 is a trial cap, so raise it for a real harvest. This is a third-party server, so readings can stop updating; check phenomenonTime. To use your own server, change the URL and clear or replace Datastream id, because 723 only exists on the KIT TERENO server. The pre-fill only fills in the form. API calls that leave these fields out still get the defaults: all observations, in server order, up to 1000 records.

Tested in Apify Console on the public Hamburg server. You fill in the form and don't write any JSON.

1. Find the sensor you want (Input → Form):

  • SensorThings API URL(s): https://iot.hamburg.de (a plain server address works).
  • Mode: the option starting with datastreams.
  • OData $filter: substringof('Photovoltaik',name) (use a word from the name of the sensor you want).
  • Max records (global cap): 5. Then click Save & start.

On Hamburg this returned 676 Photovoltaik HAW Erzeugung elektrisch (unit W, Watt) and 15717 Photovoltaik HAW Gesamtertrag elektrisch (Wh). Its @iot.id column is the number you need next.

2. Get its newest readings:

  • Mode: the option starting with observations.
  • Datastream id (observations mode): 676.
  • OData $orderby: phenomenonTime desc. Without it, many servers return their oldest readings first. Across all Hamburg sensors, the oldest readings date from 1990 (sensor 676's go back to 2020).
  • Max records (global cap): 5. Under Run options, set Maximum cost per run (for example 0.01). Then click Save & start.

3. Download the CSV: on the run, click Export, set View to SensorThings records, choose CSV, then click Download. The file has 11 columns, including datastreamName, phenomenonTime (when the reading was taken, UTC), result, unitSymbol, unitName and _source. All fields adds about 30 more raw columns.

Our run returned five readings from 2026-09-07 (1396, 1371, 1566, 1343 and 1269 W). All five match the server's own /Datastreams(676)/Observations reply. How recent the newest reading is depends on when the server last updated that sensor, so check phenomenonTime. Each run is charged per record delivered (2 sensors in step 1, 5 readings in step 2).

Why one actor covers thousands of sources

The wire format is uniform, but installs populate different optional fields — and this runner handles all of them:

  • Every field is read by key presence. A Thing without properties, a Datastream without an observedArea, an Observation without resultQuality or parameters → that column is simply null, never a crash.
  • @iot.nextLink cursor pagination. Large collections are returned page by page; the actor follows the server's cursor automatically up to your global maxRecords cap.
  • Full OData query surface. Pass $filter, $expand, $select, $orderby straight through — e.g. $filter=result gt 0, $orderby=phenomenonTime desc, $expand=Datastream,FeatureOfInterest.
  • result is any type. A reading may be a number, a string, a boolean, an array, or a complex object (OM_ComplexObservation) — it is passed through verbatim.
  • Coordinates promoted. A Thing's Point location and a Datastream's observed area are promoted to a clean [lon, lat] coordinates column, with the full GeoJSON kept in geometry / _raw.

Example input

The newest 5 readings of one sensor. This is the same as the form pre-fill:

{
"sources": ["https://sensorthings.imk-ifu.kit.edu/v1.1"],
"mode": "observations",
"datastreamId": "723",
"$orderby": "phenomenonTime desc",
"maxRecords": 5
}

A whole-server harvest, newest first across every sensor (mixed units, up to 1000 billable rows):

{
"sources": ["https://iot.hamburg.de/v1.1"],
"mode": "observations",
"$orderby": "phenomenonTime desc",
"maxRecords": 1000
}

Map an instance's assets instead:

{
"sources": ["https://sensorthings.imk-ifu.kit.edu/v1.1"],
"mode": "things"
}

Output

Every row is flat and fully nullable, with the same column set across all modes (unused columns are null), plus a lossless _raw copy of the source entity and _source / _mode / _entity provenance. Results stream to the dataset and export as JSON, CSV, Excel, or via the API.

Resilience & honesty

  • A reachable instance with no matching rows → 0 records, clean success. Nothing is ever fabricated.
  • An unreachable / DNS-failed host, an auth gate (401/403), or a non-SensorThings body is skipped with a warning and the batch continues; a run where every source is skipped fails fast so you know your input was wrong rather than getting a silent empty result.
  • No authentication is required for public instances. An optional bearer token / extra headers can be supplied for gated deployments — they are never logged.

Pricing

Pay-per-event: you are charged per record emitted to the dataset. Use maxRecords (and, for observations, datastreamId or a $filter) to bound cost precisely.

Your max charge per run is respected: the run writes and bills only the records it covers, asks the server only for as many records as that budget pays for, and makes no request at all if the budget cannot cover one record. If a later page fails, the records already delivered are kept and the run finishes with an error instead of a silent partial success.