LoRaWAN (TTN) ingest
The Things Network webhook contract
OpenSense receives LoRaWAN data as TTN webhooks. There is no direct network-server integration; if you operate your own LNS (ChirpStack, Helium console, AWS IoT Core for LoRaWAN), point its webhook output at the same URL.
Webhook URL
The dashboard issues a per-account webhook URL when you add a LoRaWAN device. Format:
The base URL is account-wide. TTN appends device path segments based on the webhook configuration — we do not require any specific suffix.
Configure in TTN
Application → Integrations → Webhooks → + Add:
| Field | Value |
|---|---|
| Format | JSON |
| Base URL | the URL above |
| Custom headers | none |
| Enabled events | Uplink message only |
Leave the other event types (join accepted, downlink queued, etc.) disabled. We do not parse them.
Payload shape
TTN posts the standard envelope. The fields OpenSense reads:
{
"end_device_ids": {
"device_id": "ns-t-3-04",
"application_ids": { "application_id": "opensense-cafe-bts" },
"dev_eui": "70B3D57ED0049ABC"
},
"received_at": "2026-05-17T08:22:00.123456789Z",
"uplink_message": {
"f_port": 1,
"decoded_payload": {
"measurements": [
{ "type": "temperature", "value": 4.13 },
{ "type": "humidity", "value": 67.8 }
],
"battery": 95
},
"rx_metadata": [ { "rssi": -97, "snr": 7.5 } ]
}
}
The fields that matter:
end_device_ids.dev_eui— primary key for device matching. This must be set up in OpenSense before the first uplink, or the message lands in theunassignedtray.uplink_message.decoded_payload— the JSON your TTN payload decoder emitted. Must conform to the generic LoRaWAN shape.uplink_message.rx_metadata[0].rssiandsnr— stored assignalandsnrauxiliary channels for diagnostics.received_at— ingest timestamp. We do not use TTN'sreceived_atas the measurement timestamp; the gateway clock can be wrong. The measurement uses our server's receive time, which is generally within 1–3 s ofreceived_at.
Authentication
The webhook URL contains a long random token (whk_…). Treat it as a
password.
For extra safety, configure TTN to send a constant custom header
(e.g. X-OpenSense-Account-Check: <random>) and ask us to enforce it
server-side. This makes a leaked URL useless without the matching
header.
Retries and ordering
- TTN retries 5xx for up to 24 h with exponential backoff. We return 5xx only on infrastructure failure; we do not 5xx on payload errors.
- TTN does not preserve ordering across retries. A late delivery
may arrive after a newer reading. OpenSense's storage is by
ts, not arrival order, so this is fine — but be aware that the dashboard's "last known reading" may briefly flip backwards in this race.
Duty cycle and cadence
EU868 limits each device to ~1 % duty cycle. Practical cadences:
| Cadence | Devices per gateway (rough) | Use case |
|---|---|---|
| 30 min | 200+ | Climate, slow drift |
| 15 min | 80–150 | Cold-chain default |
| 5 min | 30–60 | Legionella supply temp |
| < 5 min | not recommended in EU868 | Hit the duty-cycle wall |
LoRaWAN is not a substitute for WiFi if you want faster than 5 minute cadence. Use the right transport for the speed you need.
Errors
- Unassigned DevEUI — uplinks accepted but flagged. Visible in the
unassignedtray; one click associates them. - Decoded payload missing — TTN forgot to apply the decoder. The message is stored as-is with no measurements; we do not synthesise.
- Token invalid — 401 to TTN; TTN gives up after retries; the webhook stops sending.