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:

FieldValue
FormatJSON
Base URLthe URL above
Custom headersnone
Enabled eventsUplink 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 the unassigned tray.
  • uplink_message.decoded_payload — the JSON your TTN payload decoder emitted. Must conform to the generic LoRaWAN shape.
  • uplink_message.rx_metadata[0].rssi and snr — stored as signal and snr auxiliary channels for diagnostics.
  • received_at — ingest timestamp. We do not use TTN's received_at as the measurement timestamp; the gateway clock can be wrong. The measurement uses our server's receive time, which is generally within 1–3 s of received_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:

CadenceDevices per gateway (rough)Use case
30 min200+Climate, slow drift
15 min80–150Cold-chain default
5 min30–60Legionella supply temp
< 5 minnot recommended in EU868Hit 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 unassigned tray; 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.