# PostData > Free, public sensor-data API: the dweet.io successor. No signup and no API key. Devices send JSON readings over plain HTTP, HTTPS, WebSocket or MQTT-over-WebSocket; anyone can read them back as the latest value, ~92 days of numeric history, a live WebSocket stream, or a live chart in the browser. Everything is public. This is the complete reference, written for coding agents and the people working with them. The same content for humans is at https://postdata.cloud/docs, and the HTTP API is also described by an OpenAPI 3 spec at https://postdata.cloud/api.yaml. ## Quick start ```bash # 1. Get an unused device name (names are public and first come, first served) curl http://api.postdata.cloud/new # {"name":"amber-otter-4821","add":"http://api.postdata.cloud/add/amber-otter-4821", ...} # 2. Send a reading curl -X POST http://api.postdata.cloud/add/amber-otter-4821 -d '{"temp": 21.5, "hum": 60}' # 3. Read it back curl http://api.postdata.cloud/last/amber-otter-4821 # {"temp":21.5,"hum":60,"ts":1785239565} # 4. Watch it live in a browser open https://postdata.cloud/device?id=amber-otter-4821 ``` ## Rules - **Everything is public.** Anyone can read any device, and anyone who knows a device name can write to it. Never send credentials, personal data or precise locations. - **Names.** Device and metric names use letters, digits, `_`, `.` and `-`, max 64 characters. First come, first served: use `GET /new` for an unused device name rather than a generic one like `test` or `esp32-sensor`. - **1 to 10 metrics per message**, as a flat JSON object. Values are numbers or short strings (under 200 characters). Numeric strings such as `"21.5"` are stored as numbers. Strings keep only their latest value, with no history. Nested objects and arrays are rejected. - **One message per device per 30 seconds**, whichever transport you use: HTTP, WebSocket and MQTT share one limit. Faster sends get `429` with a `Retry-After` header. Put all metrics in one message instead of one message per metric. - **Numeric history is kept for ~92 days.** Devices idle longer than that disappear. - **Keys named `ts` or `timestamp` are ignored.** The server stamps reception time. ## Errors Every HTTP error is JSON with three fields: ```json { "error": "device does not exist", "hint": "Nothing has been sent to this device yet, or it has been idle for more than ~92 days. Send a reading with POST /add/{device} first.", "docs": "https://postdata.cloud/docs#last" } ``` `error` is stable and safe to match on. `hint` says how to fix the problem; `docs` links to the rule. Status codes: `400` invalid name or body, `404` unknown device, `426` WebSocket endpoint called without an upgrade, `429` rate limited (see `Retry-After`). ## Endpoints Base URL: `http://api.postdata.cloud` or `https://api.postdata.cloud`. Both work for every endpoint; WebSocket endpoints use `ws://` and `wss://`. | Method | Path | What it does | |---|---|---| | GET | `/new` | An unused device name, with its URLs | | POST, GET | `/add/{device}` | Send a reading | | GET | `/last/{device}` | Latest reading | | GET | `/past/{device}/{hours}` | Numeric history | | WS | `/live/{device}` | Live stream, and publish back | | WS | `/mqtt/{device}` | MQTT 3.1.1 and 5.0 over WebSocket | | GET | `/explore` | 20 most recently active devices | | POST | `/mcp` | Remote MCP server (see below) | ### GET /new Returns a random device name that no device is using right now, plus the URLs for it, in the scheme you called with. Nothing is reserved: the first reading sent claims the name. ```bash curl https://api.postdata.cloud/new ``` ```json { "name": "amber-otter-4821", "add": "https://api.postdata.cloud/add/amber-otter-4821", "last": "https://api.postdata.cloud/last/amber-otter-4821", "live": "wss://api.postdata.cloud/live/amber-otter-4821", "view": "https://postdata.cloud/device?id=amber-otter-4821" } ``` ### POST /add/{device} Body is a flat JSON object of metrics. Responds with the stored metrics. A `Content-Type` header is not required. The response carries a `Link` header pointing at the device's live chart. ```bash curl -X POST http://api.postdata.cloud/add/amber-otter-4821 \ -d '{"temp": 21.5, "hum": 60, "firmware": "rc2.0.2"}' # {"temp":21.5,"hum":60,"firmware":"rc2.0.2"} ``` For firmware that cannot easily send a body, the same endpoint accepts GET with metrics as query parameters: ```bash curl "http://api.postdata.cloud/add/amber-otter-4821?temp=21.5&hum=60" ``` ### GET /last/{device} The most recent metrics plus `ts`, the server reception time in unix seconds. `404` if nothing has been sent to the device. ```bash curl http://api.postdata.cloud/last/amber-otter-4821 # {"temp":21.5,"hum":60,"firmware":"rc2.0.2","ts":1785239565} ``` ### GET /past/{device}/{hours} A timestamp-ordered array of the numeric metrics. `hours` is optional: default 168 (one week), max 2208 (~92 days). Text metrics have no history. A long window at 30-second resolution is a large response (a week is up to 20,160 points); the MCP `get_history` tool returns a summary instead. ```bash curl http://api.postdata.cloud/past/amber-otter-4821/24 # [{"ts":1785153165,"temp":20.1,"hum":63},{"ts":1785153195,"temp":20.4,"hum":62}, ...] ``` ### WS /live/{device} Connect a WebSocket and receive the current reading immediately, then every new reading as it arrives, each shaped like `/last`. Send the text `ping` to get `pong` back as a keepalive. The same socket also sends: push a flat JSON object of metrics (the same body `/add` takes) and it is stored exactly like an HTTP reading. Success arrives as the broadcast itself; failures answer back on the socket and never close it, e.g. `{"error":"rate limited: retry in 24s","retryAfter":24}`. ```bash websocat wss://api.postdata.cloud/live/amber-otter-4821 ``` ```js const ws = new WebSocket("wss://api.postdata.cloud/live/amber-otter-4821"); ws.onmessage = (e) => console.log(JSON.parse(e.data)); ws.onopen = () => ws.send(JSON.stringify({ temp: 21.5 })); ``` ### WS /mqtt/{device} MQTT 3.1.1 and 5.0 over WebSocket (subprotocol `mqtt`). There is no port 1883 or 8883: use your client's WebSocket transport, with a `ws://` or `wss://` URL. The device name is in the URL; one connection carries one device. ```bash mosquitto_pub -L ws://api.postdata.cloud/mqtt/amber-otter-4821 \ -t postdata/amber-otter-4821 -m '{"temp": 21.5, "hum": 60}' ``` - Publish to `postdata/{device}` with a flat JSON object payload, or to `postdata/{device}/{metric}` with one bare value such as `21.5`. - Subscribe to `postdata/{device}` (or `postdata/{device}/#`) for live readings, delivered as the same JSON `/live` sends. Subscribing delivers the current reading immediately. - Prefer MQTT 5: on 3.1.1 a rejected publish is indistinguishable from an accepted one. MQTT 5 PUBACK reason codes: `0x97` rate limited, `0x87` topic names another device, `0x99` invalid payload. - QoS 0 and 1 only. No retained messages, wills or persistent sessions. Username and password are accepted and ignored. MQTT 5 clients are given a Server Keep Alive of 240 s. ### GET /explore The 20 most recently active devices with their latest data, as JSON (browsers get the explore page). ## MCP server `https://api.postdata.cloud/mcp` is a remote MCP server (Streamable HTTP, no authentication). It serves protocol revision 2026-07-28 and the 2025 `initialize` handshake. ```bash claude mcp add --transport http postdata https://api.postdata.cloud/mcp ``` Other clients: add a remote/HTTP MCP server with that URL and no credentials. | Tool | What it does | |---|---| | `new_device_name` | Unused device name plus its ingest, read, live and chart URLs | | `publish_reading` | Store a reading, exactly like `POST /add/{device}` (same 30 s limit) | | `get_latest` | Latest reading, its time and age, and the chart URL | | `get_history` | Per-metric min/max/mean/first/last and a downsampled series for the last N hours | | `wait_for_reading` | Block (up to 50 s) until a device sends a new reading; use after flashing firmware | | `list_recent_devices` | 20 most recently active devices with their payloads | ## Building a device with an agent A workflow that closes the loop without any account: 1. Get a name: `GET /new` or the `new_device_name` tool. 2. Write the firmware with that name, sending one message every 30 s or slower (60 s leaves headroom). 3. Flash the device. 4. Confirm it is posting: call `wait_for_reading` (or poll `GET /last/{device}` and compare `ts`). 5. Give the user the live chart: `https://postdata.cloud/device?id={device}`. If no reading arrives, the device's serial output usually says why: `429` means it sends too often, `400` means the body is not a flat JSON object, and connection failures are usually Wi-Fi or TLS. Try plain `http://` first to rule out TLS. ## Firmware recipes All recipes use plain `http://`, which every board can do. Replace `amber-otter-4821` with your own name from `GET /new`, and the sensor value with a real reading. ### ESP32 (Arduino core) ```cpp #include #include const char* WIFI_SSID = "your-ssid"; const char* WIFI_PASS = "your-password"; const char* URL = "http://api.postdata.cloud/add/amber-otter-4821"; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() != WL_CONNECTED) delay(500); } void loop() { float temp = 21.5; // read your sensor here HTTPClient http; http.begin(URL); http.addHeader("Content-Type", "application/json"); int status = http.POST(String("{\"temp\":") + temp + "}"); Serial.printf("PostData: %d %s\n", status, http.getString().c_str()); http.end(); delay(60000); // the limit is one message per 30 s } ``` ### ESP8266 (Arduino core) Same as ESP32, but with the ESP8266 headers, and `begin()` takes a `WiFiClient`: ```cpp #include #include // ...in loop(): WiFiClient client; HTTPClient http; http.begin(client, "http://api.postdata.cloud/add/amber-otter-4821"); http.addHeader("Content-Type", "application/json"); int status = http.POST("{\"temp\":21.5}"); http.end(); ``` ### MicroPython (ESP32, Raspberry Pi Pico W) ```python import network, time import requests # "urequests" on older MicroPython wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect("your-ssid", "your-password") while not wlan.isconnected(): time.sleep(0.5) URL = "http://api.postdata.cloud/add/amber-otter-4821" while True: r = requests.post(URL, json={"temp": 21.5}) # read your sensor here print("PostData:", r.status_code, r.text) r.close() time.sleep(60) ``` ### CircuitPython ```python import os, time, wifi, socketpool, adafruit_requests wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD")) requests = adafruit_requests.Session(socketpool.SocketPool(wifi.radio)) while True: r = requests.post("http://api.postdata.cloud/add/amber-otter-4821", json={"temp": 21.5}) print("PostData:", r.status_code, r.text) r.close() time.sleep(60) ``` ### ESPHome ```yaml http_request: verify_ssl: false # required on ESP8266 and RP2040; irrelevant for http:// interval: - interval: 60s then: - http_request.post: url: http://api.postdata.cloud/add/amber-otter-4821 json: temp: !lambda |- return id(room_temp).state; ``` ESPHome sends these values as strings (`"21.5"`); PostData stores numeric strings as numbers, so history and charts work. ### Python (Raspberry Pi, servers, scripts) ```python import time, requests while True: r = requests.post("http://api.postdata.cloud/add/amber-otter-4821", json={"temp": 21.5}, timeout=10) print(r.status_code, r.json()) time.sleep(60) ``` ### Anything that can open a URL ```bash curl "http://api.postdata.cloud/add/amber-otter-4821?temp=21.5" ``` ## TLS on microcontrollers Plain HTTP is accepted on purpose: TLS on a microcontroller means certificate stores, expiring roots and multi-second handshakes, and everything on PostData is public anyway. Use `https://` when the device can afford it, and follow these rules: - **Never pin the server (leaf) or intermediate certificate.** Certificates are reissued every 90 days and the issuing CA can change, so firmware with an embedded leaf certificate breaks within months. - **Trust root CAs instead.** PostData is on Cloudflare, which issues from Let's Encrypt, Google Trust Services or SSL.com. Embed these roots (about 5 KB together): ISRG Root X1 (valid until 2035), GTS Root R1 and R4 (until 2036), SSL.com Root CA RSA and ECC (until 2041). - **Set the clock first.** Certificate validation needs the current date; sync SNTP before the first handshake or validation fails. - **ESP8266 needs the full 16 KB TLS receive buffer.** Cloudflare does not negotiate `max_fragment_length`, so BearSSL's `setBufferSizes()` with 512 or 2048 bytes makes the handshake fail. ESP32 (mbedTLS) is unaffected. - `client.setInsecure()` encrypts without authenticating the server: a fair trade for public sensor data on some projects, or just use `http://`. ESP32 with a root bundle: ```cpp // roots.pem: concatenation of the root certificates above extern const char roots_pem[] asm("_binary_roots_pem_start"); WiFiClientSecure client; client.setCACert(roots_pem); // trust the roots, not the leaf HTTPClient http; http.begin(client, "https://api.postdata.cloud/add/amber-otter-4821"); http.POST("{\"temp\": 21.5}"); ``` All of this applies unchanged to `wss://` for `/live` and `/mqtt`. Client certificates (mutual TLS) are not supported. ## Coming from dweet.io dweet.io is gone. PostData answers its classic URLs with its classic response envelopes, so firmware keeps working after changing only the hostname to `api.postdata.cloud`: - `GET/POST /dweet/for/{thing}`: query parameters or a JSON body, like `/add`. `/dweet/quietly/for/{thing}` answers `204` with no body. - `GET /get/latest/dweet/for/{thing}`: the latest reading as a one-dweet array. - `GET /get/dweets/for/{thing}`: up to the 5 most recent readings of the past 24 hours, newest first. ```bash curl "https://api.postdata.cloud/dweet/for/my-thing-name?hello=world" # {"this":"succeeded","by":"dweeting","the":"dweet","with":{"thing":"my-thing-name","created":"2026-07-28T17:28:42.556Z","content":{"hello":"world"}}} ``` PostData's rules still apply (flat metrics, 30 s limit, names limited to `[A-Za-z0-9_.-]`). Locks, alerts and stored dweets are not emulated. For new firmware, use `/add`.