Full reference for larsi.org's sensor ingest API: every endpoint, parameter, and response format. New here? Start with Add a Sensor Node to get an API key and see a quickstart example first.
Two things recur throughout this API: an API key that authenticates every request, and a channel number that addresses every reading.
An API key authenticates every request and is tied to your location record (see Get an API Key). It's always exactly 16 characters: a letter (A-Z/a-z), followed by 15 more characters from A-Z, a-z, 0-9, -, or _ -- e.g. Kx7bQ2m-Z9p_LhWa. If you're validating or pre-filling it in your own device's setup (as the ESP32 sensor-node library does), match this shape:
^[A-Za-z][A-Za-z0-9_-]{15}$
Every reading is logged against a single channel number, 0-255. Channels are grouped into 16 devices of 16 channels each, so a channel number splits into a device id (0-15) and a per-device channel id (0-15):
channel = 16 × device_id + channel_id channel = device_id << 4 | channel_id
It's really just one byte, with the upper nibble as the device id and the lower nibble as the channel id:
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|
| device id | channel id | ||||||
Send a POST request to:
https://larsi.org/sensors/log
Use https:// directly rather than http:// -- an initial plain HTTP request would expose your API key before the redirect to HTTPS ever happens.
Only POST is accepted -- GET is not supported, so the API key never ends up in the web server's access log.
| Parameter | Required | Description |
|---|---|---|
key | Yes | Your API key. |
device | Yes | Device id, 0-15 (see device id above). |
data | Yes | A channel value list (see below). |
Any client that can POST a standard form-encoded body works. From the command line:
curl https://larsi.org/sensors/log \ -d "key=YOUR_KEY" \ -d "device=0" \ -d "data=72.5,45.2,1013.2"
curl -d sends a POST with a standard application/x-www-form-urlencoded body by default -- no extra headers needed.
data Parameterdata takes a comma-separated list of values, where each value's position in the list is its channel id within device -- there's no implicit default device, so it's always explicit which device a reading belongs to:
key=YOUR_KEY&device=0&data=72.5,45.2,1013.2
Logs channel 0 = 72.5, channel 1 = 45.2, channel 2 = 1013.2, all on device 0. Leave a value blank to skip that channel entirely -- it won't be logged at all, not logged as zero:
key=YOUR_KEY&device=0&data=72.5,,1013.2
Logs channel 0 = 72.5 and channel 2 = 1013.2; channel 1 is skipped.
To log a second device's channels without colliding with the first device's numbering (see the bit layout above), use its device id instead:
key=YOUR_KEY&device=1&data=72.5,45.2
Logs channel 16 = 72.5, channel 17 = 45.2.
A single request can also carry several readings at once, each with its own timestamp -- meant for a node that buffers readings (e.g. between deep-sleep cycles) and flushes them together in one connection rather than reporting every single one immediately. Send repeated data[] and t[] fields instead of the plain scalar form, one pair per reading:
key=YOUR_KEY&device=0&data[]=72.5,45.2&t[]=0&data[]=71.9,44.8&t[]=300
Each t[] is how many seconds before this request was received that reading was actually taken -- not an absolute timestamp, since a device's own clock isn't trusted for that. t[]=0 is "just now"; the example above logs one reading now and a second from 5 minutes ago. data[] and t[] must have the same number of entries, paired by position. A t[] too large for this device's own reporting cadence to plausibly explain (more than 64 log intervals ago) is dropped -- that entry alone doesn't get logged, but the rest of the batch still does.
The response is plain text. Every request starts with a line confirming the timestamp used, followed by Data logged once at least one channel was actually inserted (a batched request instead reports how many of its readings logged and how many were dropped):
Logging... 2026-08-13 20:15:32 (1786652132) Data logged
A successful request may also carry a one-shot test command, set by hand server-side against a specific device for manual testing (e.g. "open the setup portal on next boot") -- not a general remote-command channel, and never present unless something logged successfully:
Logging... 2026-08-13 20:15:32 (1786652132) Data logged Command: open_portal
On failure, nothing is logged and one of these appears instead:
| Message | Cause |
|---|---|
POST required | Request wasn't sent as POST. |
Key, device, and data arguments required | Missing key, device, or data parameter. |
Key not found | Invalid key. |
Invalid device argument | device isn't a plain non-negative number. |
Invalid data/t argument | Batched data[]/t[] weren't both present with matching lengths. |
Provisioning registers a device and its channels ahead of time, so reports show real labels (a device name and each channel's sensor/property/unit) instead of raw numbers. It's optional -- log works fine with no provisioning at all. Server-side, name, logInterval, and reportEvery are all required on every call and always overwrite the stored value (a blank name makes the Sensors table hard to read, so it's rejected outright); channels' sensor/property/unit fields, by contrast, only update the row when a call sends a real value and never get blanked out by a call that leaves one empty -- which makes it safe to call repeatedly with the same values, though there's no need to call it on every boot either: the reference ESP32 sensor-node firmware only calls this once, right after its setup portal saves new settings. Every call also resets the device's Zeus alerting: zeus_minutes is set to 12×reportEvery×logInterval and zeus_successful to 1, so a device stays flagged as down for at most 12 missed report cycles (a report being reportEvery buffered readings, for a node that batches -- see Batched Reports above) and re-provisioning after a reflash clears any stale alert.
Send a POST request to:
https://larsi.org/sensors/provision
| Parameter | Required | Description |
|---|---|---|
key | Yes | Your API key. |
device | Yes | Device id, 0-15 (see device id above). |
name | Yes | Device name -- free text, e.g. Weather Basement. There's no separate location field, so the convention is "Name (and Location)": bake a location into the name if a station has (or might grow) more than one device. The reference ESP32 sensor-node firmware makes this field required in its own setup portal too (defaulting to Weather plus the device's last 6 MAC hex digits if left blank), so it's never actually blank by the time a real node calls this endpoint. |
mac | No | Device's hardware MAC address, 12 hex digits with no separators (e.g. 404CCA4C3D74). Unlike the other fields here, this can't be filled in server-side -- a remote HTTP request only ever carries the client's IP, never its link-layer address (routing strips and replaces the Ethernet frame at every hop, and even on the same LAN a web server has no API to read one) -- so it's blank on a device whose firmware doesn't send it yet, and never gets blanked out by a call that leaves it empty (same non-empty-upsert rule as channels' fields below). This is inventory/identification only, not an authentication signal -- it's self-reported by the same request presenting the key, so anyone who already has a valid key could send any mac value they like. Don't use it to cross-check a request's real origin. |
logInterval | Yes | Minutes between wake/read cycles, 1-1440. |
reportEvery | Yes | How many cycles between log reports, 1-12 -- 1 means every cycle is reported immediately (no buffering). Used together with logInterval to set zeus_minutes (12×reportEvery×logInterval) for per-device alerting -- see above. |
channels | Yes | A |-separated list of channel_id,sensor,property,unit quadruples. |
curl https://larsi.org/sensors/provision \ -d "key=YOUR_KEY" \ -d "device=0" \ -d "name=Weather Basement" \ -d "mac=404CCA4C3D74" \ -d "logInterval=5" \ -d "reportEvery=1" \ -d "channels=0,BME280,Temperature,C|1,BME280,Humidity,%"
Each entry in channels is channel_id,sensor,property,unit -- e.g. 0,BME280,Temperature,C registers channel 0 as a BME280 sensor reporting Temperature in C. sensor is the physical hardware's model name (e.g. BME280, DS18B20, SHT15). channel_id must be 0-15 (see Data Channel above).
The response is plain text, confirming what was created or updated:
Provisioning... Provisioned Device mydevice[0]: created, 2 new channel(s) added, 0 channel(s) updated
The device status is created, updated, or unchanged, depending on whether this call's name actually changed anything.
On failure, nothing is created and one of these appears instead:
| Message | Cause |
|---|---|
POST required | Request wasn't sent as POST. |
Key, device, name, logInterval, reportEvery, and channels arguments required | Missing key, device, name, logInterval, reportEvery, or channels parameter. |
Key not found | Invalid key. |
Invalid device argument | device outside 0-15. |
Name argument required | name was present but blank (or all whitespace). |
Invalid logInterval argument | logInterval outside 1-1440. |
Invalid reportEvery argument | reportEvery outside 1-12. |
Some devices need more configuration than provision's fixed fields cover -- e.g. the ESP32 sensor-node library's DS18B20GridNode example, which reads a per-deployment grid layout of probe ROM IDs. config serves that kind of file back to the device it belongs to, gated by the same API key as every other endpoint here.
Unlike log/provision, this isn't self-service: the config file itself is hand-authored server-side per device, not something you can register through this API. Send a POST request to:
https://larsi.org/sensors/config.php
| Parameter | Required | Description |
|---|---|---|
key | Yes | Your API key. |
device | Yes | Device id, 0-15 (see device id above). |
The response is the config file's raw content (no preamble, unlike log/provision's human-readable responses -- this one's meant to be parsed directly) on success, or a non-200 status with a plain-text error otherwise: 400 for a missing/invalid key or device, 404 if no config file exists yet for that device.