@shmaplex/xplant-sdk 0.7.0 is out: what's new →
xPlantAPI

Sensors and devices

Register a device, give it a device token, and post batched sensor readings, heartbeats and events from a Raspberry Pi or an ESP32.

Credential: a device token on the device; a workspace key with write:devices to set it up, and read:sensor_readings to read data back. Endpoints: Submit sensor readings, Send a heartbeat, Record a device event, List sensor readings.

1. Set the device up

From your own computer, with a workspace key: register the device and create its token. Put the device id and the xpd_ token on the device. The device never sees a workspace key.

How many devices you can register depends on your plan; registering past the allowance answers 402 DEVICE_LIMIT_REACHED.

A gateway that reads several probes is usually one device posting several reading types. A device has one channel per reading type: readings have no probe field, so two probes of the same type on one device would collide. Register the second one as its own device. If you'd rather see each probe as its own device, register each one and give the gateway a token per device.

2. Post readings, in batches

A reading is device_id, type, value and unit, plus optional recorded_at, external_id, room_id and notes. Send up to 500 in one request as {"readings": [...]}.

curl -X POST https://app.xplantpro.com/api/v1/sensor-readings \
  -H "Authorization: Bearer $XPLANT_DEVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "readings": [
      {
        "device_id": "5f7a9c1e-3b5d-4f7a-9c1e-3b5d7f9a1c3e",
        "type": "temperature", "value": 23.4, "unit": "C",
        "recorded_at": "2026-09-25T12:00:00Z",
        "external_id": "gw1-temperature-20260925T120000"
      },
      {
        "device_id": "5f7a9c1e-3b5d-4f7a-9c1e-3b5d7f9a1c3e",
        "type": "humidity", "value": 71.2, "unit": "%",
        "recorded_at": "2026-09-25T12:00:00Z",
        "external_id": "gw1-humidity-20260925T120000"
      }
    ]
  }'

Batch your posts

One request per reading multiplies your request count for the same data and reaches the rate limits far sooner. A lab with four devices, five channels each, sampling once a minute would send about 864,000 requests a month unbatched, against a few thousand batched.

Buffer locally and flush on whichever comes first:

  • every 30–60 seconds, for sensors that sample about once a minute, or
  • once 500 readings are queued (the per-request maximum).

A single reading without the readings wrapper also works. Treat it as the fallback for a device that can only ever hold one pending reading, not as the default.

Send recorded_at as an ISO 8601 timestamp with Z or an offset, for example 2026-09-25T12:00:00.000Z or 2026-09-25T14:00:00+02:00: the time the reading was taken, not the time you send it.

Make retries harmless. Put external_id and recorded_at on every reading. A reading that repeats one already stored for the same device (same external_id and recorded_at) is skipped rather than stored twice: in a batch it's left out of the response list, and a single reading answers 201 with the reading already stored and meta.duplicate: true. (Idempotency-Key isn't used on this endpoint.) Build external_id from something stable: the device, the channel and the sample time.

A buffering gateway in Python

import os
import time
from datetime import datetime, timezone

import requests

URL = "https://app.xplantpro.com/api/v1/sensor-readings"
HEADERS = {"Authorization": f"Bearer {os.environ['XPLANT_DEVICE_TOKEN']}"}
DEVICE_ID = os.environ["XPLANT_DEVICE_ID"]
FLUSH_EVERY_SECONDS = 30
MAX_BATCH = 500  # the per-request maximum

buffer = []


def queue(reading_type, value, unit):
    now = datetime.now(timezone.utc)
    buffer.append({
        "device_id": DEVICE_ID,
        "type": reading_type,
        "value": value,
        "unit": unit,
        "recorded_at": now.isoformat(timespec="milliseconds").replace("+00:00", "Z"),
        # Stable per device, channel and second: re-sending it is a no-op.
        "external_id": f"{DEVICE_ID}-{reading_type}-{int(now.timestamp())}",
    })
    if len(buffer) >= MAX_BATCH:
        flush()


def flush():
    if not buffer:
        return
    batch = buffer[:MAX_BATCH]
    for _ in range(3):
        try:
            resp = requests.post(URL, headers=HEADERS, json={"readings": batch}, timeout=10)
        except requests.RequestException:
            time.sleep(5)
            continue
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", "5")))
            continue
        if resp.ok:
            del buffer[: len(batch)]
        return
    # Still failing: keep the readings and try again on the next flush.


last_flush = time.monotonic()
while True:
    for reading_type, value, unit in read_sensors():  # your sensor code
        queue(reading_type, value, unit)
    if time.monotonic() - last_flush >= FLUSH_EVERY_SECONDS:
        flush()
        last_flush = time.monotonic()
    time.sleep(1)

The Raspberry Pi gateway in this repository is a complete, installable version. In JavaScript, the SDK's sensorReadings.buffer() does all of this for you.

3. Heartbeats and events

A heartbeat tells xPlant the device is alive and updates its last-seen time. Send one every few minutes, or with each flush.

curl -X POST "https://app.xplantpro.com/api/v1/devices/$XPLANT_DEVICE_ID/heartbeat" \
  -H "Authorization: Bearer $XPLANT_DEVICE_TOKEN"

A device event records something the device noticed or did: a door opening, a relay switching, a status change. Send an external_id so a retry is harmless: a repeat under the same external_id answers 201 with the event already stored and meta.duplicate: true. Without one, a retried request is recorded twice.

curl -X POST https://app.xplantpro.com/api/v1/device-events \
  -H "Authorization: Bearer $XPLANT_DEVICE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"device_id": "'"$XPLANT_DEVICE_ID"'", "event_type": "door_opened", "payload": {"door": "Grow room 2"}, "external_id": "pi-3-door-2-20260925T081500Z"}'

4. Read readings back

From a dashboard or a script, with a workspace key holding read:sensor_readings (not the device token). Reading readings back through the API needs xPlant+ Teams or Enterprise; on Hobby and Pro Lab the data is visible in xPlant.

curl "https://app.xplantpro.com/api/v1/sensor-readings?device_id=$DEVICE_ID&type=temperature&since=2026-09-24T00:00:00Z&until=2026-09-25T00:00:00Z&limit=1000" \
  -H "Authorization: Bearer $XPLANT_API_KEY"

Readings come newest first. Filter by device_id, room_id and type, and set the window with since and until (both inclusive). A page holds 100 readings by default and up to 1,000; follow meta.next_cursor for the rest, as on every list.

Edit on GitHub

On this page