Send your first packet
You already have firmware — a C project, a Python script on a Pi, a gateway you cannot reprogram but can point at a URL. You do not need our client, our flasher, or an SDK. Send bytes to one of five endpoints and the device shows up here.
1. Pick a hardware id
The hardware id is the board’s name on the wire: it travels in the URL path, the MQTT topic or the hello line, and every packet arrives under it. Anything free-form works — a MAC, a serial number, a word — as long as it has no spaces and no /, ? or #, because it goes into a web address.
Use whatever the hardware already calls itself. If it is a fixed MAC printed on the board, that is the right answer: it is the one string you will still be able to find the device by in six months.
2. Send bytes
Every endpoint is named for its transport, and the payload is raw bytes — no envelope, no JSON wrapper, no content-type we care about. What those bytes mean is your decoder’s business, later.
| Endpoint | How to send | TLS |
|---|---|---|
https.warbletiot.com | POST /ingest/{hwId}, body = payload | yes |
http.warbletiot.com:80 | the same POST, for a chip with no TLS stack | no |
mqtt.warbletiot.com:1883 | publish chirp/{hwId}/up | no |
mqtt.warbletiot.com:8883 | the same publish, same topics, same credential | yes |
tcp.warbletiot.com:7700 | hello line, then one payload per line | no |
udp.warbletiot.com:7701 | one datagram: prefix, then payload bytes | no |
HTTPS and MQTTS are the two TLS paths. Port 8883 is the same broker as 1883 — same topics, same username and password — with the wire closed, so an MQTT board moves a port and changes nothing else about itself. There is still no TLS-wrapped TCP and there will never be TLS over UDP, so http., tcp., udp. and plain mqtt.:1883 stay readable to anyone on the path. All of them can be Signed; only the two TLS rows can be Encrypted.
TLS is not the only thing this choice decides. If the platform will ever need to tell the board to do something, the transport also fixes how long that takes — from under a second to never. That is §6, and it is worth reading before you pick, because changing your mind later means changing the firmware.
Here is each one as a single line you can paste into a terminal right now. Replace my-board with your id.
# HTTPS — the default. Any bytes; two here that decode to 21.4 °C.
curl -X POST https://https.warbletiot.com/ingest/my-board --data-binary $'\x00\xd6'
# Plain HTTP — same request, no TLS. Use THIS host, not https. with http://:
# https.warbletiot.com redirects, and naive firmware loses the body on a redirect.
curl -X POST http://http.warbletiot.com/ingest/my-board --data-binary $'\x00\xd6'
# MQTT — username is the hardware id; NO password flag until it is claimed.
# Leave -P off entirely: -P "" sets the flag over an empty value, which is a
# protocol violation, and the broker answers CONNACK 0x82 instead of accepting.
mosquitto_pub -h mqtt.warbletiot.com -p 1883 -u my-board \
-t chirp/my-board/up -m hello
# MQTTS — the same line with the wire closed. --capath is your OS trust store.
mosquitto_pub -h mqtt.warbletiot.com -p 8883 --capath /etc/ssl/certs -u my-board \
-t chirp/my-board/up -m hello
# TCP — hello line first, then one payload per line. "-" is the no-token field.
printf 'CHIRP1 my-board -\nhello\n' | nc tcp.warbletiot.com 7700
# UDP — one datagram: "CHIRP1 {hwId} {token} " then the raw payload bytes.
printf 'CHIRP1 my-board - hello' | nc -u -w1 udp.warbletiot.com 7701On TCP and UDP the third field is the token and - means “I don’t have one”. On MQTT it is the absence of the password flag — not an empty password, which the broker refuses. On HTTP it is simply the absence of the X-Chirp-Token header (or of ?token= on the URL). Once the device is claimed, that field is where its token goes — nothing else about the framing changes. Binary on TCP rides as one line prefixed b64:; everywhere else it is the raw bytes.
The same thing on a board is the Open starter, in MicroPython, Arduino or C: it posts a line of text to http.warbletiot.com every 10 seconds, asks for a command first, and says back whatever you type in its Message box. Its C file has the UDP form too.
Say what the message is. A packet is bytes, and bytes cannot say whether they are the picture you asked for or the hourly one. A tag can: an optional label the device sends beside the payload, up to 32 characters of letters, digits, _, . and -. The pattern worth copying is that a device echoes the command it is answering — a camera woken by snap posts its frame tagged snap, and its hourly picture tagged heartbeat. Then nothing has to be paired up afterwards by guessing from the clock.
# HTTPS — a request header.
curl -X POST https://https.warbletiot.com/ingest/my-board -H 'X-Chirp-Tag: snap' --data-binary @frame.jpg
# MQTT — one extra topic level, and only one.
mosquitto_pub -h mqtt.warbletiot.com -p 1883 -u my-board -t chirp/my-board/up/snap -m hello
# TCP — open with CHIRP1T, then every line is "<tag> <payload>" ("-" for none).
printf 'CHIRP1T my-board -
snap hello
- just a heartbeat
' | nc tcp.warbletiot.com 7700
# UDP — the CHIRP1T datagram, with the tag as its own field.
printf 'CHIRP1T my-board - snap hello' | nc -u -w1 udp.warbletiot.com 7701The tag never touches the payload — your bytes are stored, decoded and served back exactly as they are without one. Sending no tag is always fine and is what every device did before this existed. A tag that breaks the rule is refused rather than trimmed: over HTTP you get a 400 that quotes the rule back at you, so a typo shows up on the first packet instead of becoming a label you never sent. If your device signs its messages (Signed), the tag is inside the signature — sign nonce + "\n" + "tag=" + tag + "\n" + body, with both lines always present and empty fields left empty, so a message with no tag and no nonce signs "\ntag=\n" + body.
3. Read the answer
A first packet from an id nobody owns comes back 202 {"status":"unclaimed"}. That is a success, and it is the specific success you want: the bytes were accepted and the platform has recorded a sighting — the id, the transport, a sample of the payload, and when it arrived. Nothing is stored against an account yet, because there is no account attached to it yet.
A plain 202 with no body means the device is already claimed and its message is stored. The other two answers worth knowing: 401 is a bad or missing token on a device that HAS one, and 429 is throttling — it carries Retry-After in seconds, and the correct response is to wait that long, not to retry harder.
What it costs to store
Rate limits bound how fast a device may speak. A budget bounds how much it may leave behind. Each device gets 64 MiB of stored payload a day, and each account 500,000 stored messages a day. Both windows are the UTC day and both reset at midnight UTC, which is why a budget 429 can promise an exact time rather than a guess.
The numbers were measured against the demos rather than picked. A sensor sending two bytes every 30 seconds stores 5.8 KB a day — 0.008% of the budget. The camera demo, one QVGA frame a minute at 13,355 bytes a frame, stores 19.2 MB a day — 28.6%, so it fits with a 3.5× margin. What the default refuses is the edit that camera’s README already warns about: the same loop at UXGA is 144–288 MB a day, and hearing about that at 80% is better than hearing about it from an invoice.
- A refused message is not stored — not trimmed, not truncated, not partially kept. Over HTTP you get
429withRetry-Afterand the message device storage budget spent for today, deliberately different from the rate-limit wording: too many messages and no room left today have opposite fixes. X-Chirp-Budgetcounts down — it rides every202and the budget429the core answers, carrying the bytes remaining in that device’s window as a bare decimal. It is absent when no budget applies, and absent is not0. The public plain-HTTP gateway onhttp.warbletiot.com:80passes it back too. Header names are case-insensitive, and the HTTPS door delivers this one asx-chirp-budget, so read it without regard to case.- On MQTT, UDP and the public TCP gateway a refusal is silent — QoS 0 has no per-message reply and a datagram has no reply channel at all, so the publish is dropped and counted. The portal is where you see it: a refused device wears a budget refused badge on its dashboard card and a callout on its own page, which is the difference between a board being refused and a board being unplugged.
- Two things are never budgeted — a downlink poll (it hands you your own bytes back), and an unclaimed sighting (there is no account to charge). An idempotent resend spends nothing either: it is answered from the dedupe table and stores nothing.
You are emailed once when a device passes 80% of its byte budget and once when a message is actually refused — at the address on Settings, where today’s numbers and your plan are also shown.
Sightings expire after 72 hours.
An unclaimed sighting is swept 72 hours after the device’s last packet. If you send one packet on a Friday and come looking on Tuesday, the search will be empty and nothing is broken — power the board on, let it send again, and search once it has. A device that reports on a schedule keeps its own sighting alive.
4. Find it and claim it
Sightings are not browsable: an inbox anyone could page through would let anyone enumerate every device in the world. You look yours up by its id, on Devices → Already sending? Find it.
- 1Search the id, or at least its last four characters. Four is a hard floor — a shorter suffix is refused, because a two-character search is a fishing net. The whole id works too; it is its own tail.
- 2Claim it. The device moves into your account, keeps its history, and a token is minted and shown to you once.
Claiming does not change what your firmware has to send. A searched claim binds the device at Open — the level of the sighting it was built on — so the very next tokenless packet, which is the same packet that produced the sighting, is still accepted. This is the whole point of the path: it exists for hardware that sends no token — gear you cannot reprogram, and firmware that stays at Open like the Open starter — and a claim that demanded a token would kill exactly that hardware, silently.
The token is real and it is yours whenever you want it. Put it on the board, and on a spec designed for Token or higher the device climbs to Token on the first message that carries it — by itself, with no gap. On a spec designed for Open it stays at Open. Attaching a spec whose design is higher does not force anything either: the design is a target the device walks up to on evidence. See Moving up without going dark.
Empty searches back off, hard.
Consecutive empty searches and failed claims shrink your rate budget exponentially — it is the defence against someone walking the id space. One or two misses while you get the id right costs you nothing. A script that retries in a loop will lock itself out of the endpoint it is trying to use, and the fix is to stop and check the id, not to keep going.
5. Then: name the bytes
A claimed device stores raw payloads and shows them as hex. To get named fields on a chart, the device needs a spec with a decoder on it — a short function in Starlark (Python-like) that turns your bytes into a flat dict. The device page’s Spec picker is where that happens — choose a spec or make one there. The page then offers Write its decoder, which opens that spec’s editor with your device’s own last payload already loaded.
Decoding happens as a packet arrives, and never afterwards. Attaching a spec or saving a decoder applies to the packets that arrive from that moment on; the raw messages already stored stay raw, and no amount of waiting will name them. So the first named fields you see are the ones on the device’s next message — which is what the device page is telling you when it says it is waiting for one.
- Sending on a long period? The wait is the period: a board on a 15-minute beat shows its first named fields 15 minutes after you save the decoder. Save it anyway and go and do something else.
- Two bytes, big-endian, tenths of a degree is the built-in example:
0x00 0xD6→21.4. It is there so you can see the whole loop work before writing your own. - Ready for more than Open? Security levels is the ladder, and provisioning over USB is how the portal puts a token or a signing key on a board without you retyping either.
6. If the platform has to talk back
Everything above is the uplink. The other direction — POST /v1/devices/{id}/send, a dashboard button, or a routine calling send() — puts bytes in a queue, and how long they sit there is decided by the transport you chose in step 2 and by nothing else. These are measured on real boards, not modelled.
| Transport | How the bytes get there | Time to the device |
|---|---|---|
| HTTPS / HTTP | the device asks: POST /ingest/{hwId}/down | your own poll interval — you set it, and it is the whole latency |
| MQTT / MQTTS | broker push on chirp/{hwId}/down | 0.5–0.9 s from the REST call to bytes at the board |
| raw TCP | the relay holds your session and pushes down: lines | up to 2 s — the relay polls the core on a 2 s tick |
| UDP | nothing: the datagram gateway has no reply channel | never — there is no downlink over UDP |
The MQTT numbers are two boards on the bench. An XIAO ESP32-C6 toggling an LED answers in 0.52–0.80 s end to end. An ESP32-WROVER camera told to snap has the command in 0.66–0.73 s, spends 0.22–0.23 s capturing and encoding a QVGA JPEG, and the new frame is readable from /v1/devices/{id}/raw/latest 1.16–1.28 s after the call — so about a second of that is the camera, not the wire. A board that keeps its MQTT session open pays the push latency once; one that reconnects per message pays the handshake too, and on MQTTS that is a TLS handshake.
Which to pick. If the device only ever reports — a thermometer, a meter, a counter — take HTTPS and stop reading: it is one request, every language has a client, and a downlink you never use costs nothing. If something in the world has to happen because of something the platform decided, take MQTTS: it is the only transport here where the platform can reach a board that did not just speak, and a second is as good as it gets without holding a socket open yourself. Raw TCP is for a chip with no TLS stack and no MQTT library that still has to be reachable — two seconds, and the relay is doing the polling instead of your firmware. UDP is for the mesh and the battery: cheapest packet on the list, and the platform can never answer it, so anything that must be commanded should not be on it. Sending on a long period does not change this — on HTTP a 15-minute beat is also a 15-minute command latency, which is the trap this table exists to name.
A command a board collects through the plain-HTTP gateway shows on its device page as Sent, not Collected: the gateway asked for it, and what happens after that is not something Warblet can see. A reply from the board, like the Open starter saying back what you typed, is how you know it arrived.
7. One rule per device that speaks
One rule per device that speaks. A rule runs when its device sends a message, can read any device’s state, and can command any device — so the number of rules you need is the number of devices that say something, not the number of things that happen. Inside temperature above outside temperature turns a fan on is one rule: the porch sensor speaks, the rule reads the yard sensor with state.get() and commands the fan controller with send(), and the fan never speaks at all. A button that makes a camera take a picture and mail it is two, because two devices speak: the button’s rule sends snap to the camera, and the camera’s rule decides whether the frame is worth mailing. And a device says why it is sending with a tag — an optional short string beside the payload, sent as X-Chirp-Tag on HTTP or as one extra topic level chirp/{hwId}/up/<tag> on MQTT, signed along with the body on a Signed device, and read as event["tag"] in a rule. The camera tags the frame it captured for a snap as snap and its timer frame as heartbeat, so the second rule is one comparison rather than an attempt to work out which frame belongs to which press.