documentation

base url api.postdata.cloud http:// or https:// no key required
01

the rules

  • Names. Devices and metrics use letters, numbers, _, ., - (max 64 chars). First come, first served.
  • 1 to 10 metrics per message. Values are numbers, or short strings — strings keep only their last value, with no history.
  • One message per device per 30 seconds, whichever way you send it — HTTP, WebSocket and MQTT all share the one limit. Faster posts get a 429 with a Retry-After header.
  • Numeric history is retained for ~92 days. Everything is public.
  • Keys named ts or timestamp are ignored — the server stamps the reception time.
02

endpoints

six, in total
POST /add/{device} Send a reading.
GET /last/{device} Latest reading.
GET /past/{device}/{hours} History.
WS /live/{device} Live stream, and publish back.
WS /mqtt/{device} MQTT 3.1.1 and 5.0.
GET /explore Recent devices.

POST /add/{device} — send a reading

Body is a flat JSON object of metrics. Responds with the stored metrics.

POST a body
curl -X POST http://api.postdata.cloud/add/home-geo-station \
  -d '{"temp": 21.5, "hum": 60, "firmware": "rc2.0.2"}'

{"temp":21.5,"hum":60,"firmware":"rc2.0.2"}

Too basic a firmware for POST bodies? The same endpoint accepts GET with query parameters:

or just GET
curl "http://api.postdata.cloud/add/home-geo-station?temp=21.5&hum=60"

Errors: 400 invalid name or body · 429 rate limited (see Retry-After).

GET /last/{device} — latest reading

The most recent metrics plus ts, the server reception time in unix seconds.

latest values
curl http://api.postdata.cloud/last/home-geo-station

{"temp":21.5,"hum":60,"firmware":"rc2.0.2","ts":1785239565}

Errors: 404 unknown device.

GET /past/{device}/{hours} — history

Timestamp-ordered array of the numeric metrics. hours is optional (default 168 = one week, max 2208 ≈ 92 days). Text metrics carry no history.

last 24 hours
curl http://api.postdata.cloud/past/home-geo-station/24

[{"ts":1785153165,"temp":20.1,"hum":63},
 {"ts":1785153195,"temp":20.4,"hum":62},
 ...]

Errors: 404 unknown device.

GET /live/{device} — live stream (WebSocket)

Connect a WebSocket and receive the current reading immediately, then every new reading as it arrives — each message a JSON object shaped like /last. Send ping and you get pong back if you need a keepalive.

from a shell
websocat wss://api.postdata.cloud/live/home-geo-station

{"temp":21.5,"hum":60,"ts":1785239565}
{"temp":21.7,"hum":59,"ts":1785239595}
...
from a browser
const ws = new WebSocket('wss://api.postdata.cloud/live/home-geo-station');
ws.onmessage = (e) => console.log(JSON.parse(e.data));

The same socket also sends. Push a flat JSON object of metrics — the identical body /add takes — and it is stored exactly as an HTTP reading would be. One connection, both directions, no reconnect per reading.

publish over the same socket
ws.send(JSON.stringify({ temp: 21.5, hum: 60 }));

// Success arrives as the broadcast itself, stamped by the server:
// {"temp":21.5,"hum":60,"ts":1785239625}
// Failures answer back instead, and never close the socket:
// {"error":"rate limited: retry in 24s","retryAfter":24}

WS /mqtt/{device} — MQTT (3.1.1 and 5.0)

Already speak MQTT? Point your client at /mqtt/{device} and keep your existing code. The device name lives in the URL, not the topic, and one connection carries one device.

mosquitto · websocket transport
mosquitto_pub -L ws://api.postdata.cloud/mqtt/home-geo-station \
  -t postdata/home-geo-station -m '{"temp": 21.5, "hum": 60}'

Two topic shapes, whichever your firmware already produces:

  • postdata/{device} — payload is a flat JSON object, exactly like /add.
  • postdata/{device}/{metric} — payload is one bare value, e.g. 21.5.
  • Subscribe to postdata/{device} (or postdata/{device}/#) for live readings, delivered as the same JSON /live sends.

Use MQTT 5 if your client supports it. MQTT 3.1.1's PUBACK has no reason-code field, so a rejected publish is indistinguishable from an accepted one — your reading silently vanishes. On MQTT 5 you get told: 0x97 when the 30-second rate limit turned you down, 0x87 for a topic naming another device, 0x99 when the payload failed validation.

What is deliberately missing, because a public last-value store has no use for it:

  • QoS 2. QoS 0 and 1 only; MQTT 5 clients are told this up front via Maximum QoS.
  • Retained messages, wills, persistent sessions. Subscribing already delivers the current reading immediately, which is what retain would have bought you.
  • Credentials. CONNECT's username and password are accepted and ignored. Every device here is public.

We answer PINGREQ, but MQTT 5 clients are handed a Server Keep Alive of 240 s that overrides whatever they configured — long enough to stay cheap, short enough to survive the NAT timeouts that quietly kill idle connections.

GET /explore — recent devices

The 20 most recently active devices with their latest data. Powers the explore page.

03

coming from dweet.io?

dweet.io is gone, but your things don't need to know. PostData answers dweet's classic URLs with dweet's classic envelopes — point your firmware at api.postdata.cloud instead of dweet.io and it keeps dweeting:

dweet, unchanged
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"}}}
  • GET/POST /dweet/for/{thing} — query parameters or a JSON body, exactly like /add. /dweet/quietly/for/{thing} works too and answers 204 with no body.
  • GET /get/latest/dweet/for/{thing} — the latest reading as a one-dweet array (dweet's /last).
  • GET /get/dweets/for/{thing} — up to the 5 most recent readings of the past 24 hours, newest first.

PostData's rules still apply: flat metrics only (1–10 keys), one message per 30 seconds (a 429 failure envelope with Retry-After), names limited to [A-Za-z0-9_.-]. Locks, alerts and stored dweets are not emulated — everything here is public. For new firmware, prefer the native /add family above.

04

a note on plain http

TLS on microcontrollers means certificate stores, expiring root CAs and multi-second handshakes. PostData deliberately accepts unencrypted HTTP on port 80 so constrained devices can publish with zero ceremony — everything on PostData is public anyway. Use https:// whenever your device can afford it.

05

tls on microcontrollers, done right

Want encryption anyway? Great — just don't pin the server certificate. Public certificates are short-lived (90 days today, and industry rules are shrinking that further every year), so firmware that embeds the certificate it saw at build time is guaranteed to brick within months.

Embed the root CA instead. Roots live for decades, and every renewal underneath them keeps validating. PostData runs on Cloudflare, whose certificates are issued by one of three public CAs, so embed these roots (about 5 KB total, all available from each CA's site) and you are covered until well into the 2030s:

On ESP32 (Arduino core) it looks like this:

esp32 · arduino
// 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/my-sensor");
http.POST("{\"temp\": 21.5}");

Three classic gotchas:

  • Set the clock first. Certificate validation compares dates, and a device with no RTC boots in 1970. Sync via SNTP once before the first TLS handshake or validation will fail.
  • Prefer ECDSA. Smaller certificates, faster handshakes, less RAM — most embedded TLS stacks handle it well. An RSA chain is served automatically to clients that don't offer ECDSA, so older stacks still connect.
  • ESP8266 needs a big TLS buffer. Cloudflare does not negotiate the max_fragment_length extension, so BearSSL cannot shrink its receive buffer to 512 or 2048 bytes the way setBufferSizes() invites you to. Give it the full 16 KB or the handshake fails with no useful error. ESP32 (mbedTLS) is unaffected.

And if even a root bundle is too much ceremony: some stacks offer "insecure" TLS (client.setInsecure()) — encrypted against passive sniffing but not authenticated, so a man-in-the-middle could tamper with it. For public sensor data that is sometimes a fair trade. Or just use plain http:// and move on with your life.

websockets and mqtt

All of the above applies unchanged to /live and /mqtt: wss:// and ws:// are the same two doors as https:// and http://, on the same ports, presenting the same certificate. Nothing extra to configure.

One thing to know if you are used to a normal broker: there is no port 8883, and no 1883. MQTT here is always MQTT-over-WebSocket, because PostData runs on Workers, which accept HTTP and HTTPS only. Clients that can only open a raw MQTT socket cannot connect at all; clients that support a WebSocket transport need it switched on, and that is the whole migration. In practice that means a URL scheme of ws:// or wss:// rather than mqtt:// or mqtts://.

Client certificates are not supported either, so don't reach for mutual TLS to identify a device. There is nothing to authenticate against — every device on PostData is public, and CONNECT's username and password fields are accepted and thrown away.