[{"data":1,"prerenderedAt":585},["ShallowReactive",2],{"doc-\u002Fapi\u002Fingest-lorawan":3},{"id":4,"title":5,"body":6,"description":575,"edit":576,"extension":577,"meta":578,"navigation":579,"path":580,"seo":581,"stem":582,"vertical":576,"weight":583,"__hash__":584},"content\u002Fapi\u002Fingest-lorawan.md","LoRaWAN (TTN) ingest",{"type":7,"value":8,"toc":566},"minimark",[9,13,18,21,33,36,40,47,102,105,109,112,361,364,423,427,434,445,449,464,468,471,532,535,539,562],[10,11,12],"p",{},"OpenSense receives LoRaWAN data as TTN webhooks. There is no direct\nnetwork-server integration; if you operate your own LNS (ChirpStack,\nHelium console, AWS IoT Core for LoRaWAN), point its webhook output at\nthe same URL.",[14,15,17],"h2",{"id":16},"webhook-url","Webhook URL",[10,19,20],{},"The dashboard issues a per-account webhook URL when you add a LoRaWAN\ndevice. Format:",[22,23,25],"cmd",{"label":24},"$ ttn webhook base url",[10,26,27],{},[28,29,30],"a",{"href":30,"rel":31},"https:\u002F\u002Fapi.opensense.murzin.digital\u002Fv1\u002Fingest\u002Florawan?token=whk_4f3c1a2b",[32],"nofollow",[10,34,35],{},"The base URL is account-wide. TTN appends device path segments based on\nthe webhook configuration — we do not require any specific suffix.",[14,37,39],{"id":38},"configure-in-ttn","Configure in TTN",[10,41,42,46],{},[43,44,45],"code",{},"Application → Integrations → Webhooks → + Add",":",[48,49,50,63],"table",{},[51,52,53],"thead",{},[54,55,56,60],"tr",{},[57,58,59],"th",{},"Field",[57,61,62],{},"Value",[64,65,66,75,83,91],"tbody",{},[54,67,68,72],{},[69,70,71],"td",{},"Format",[69,73,74],{},"JSON",[54,76,77,80],{},[69,78,79],{},"Base URL",[69,81,82],{},"the URL above",[54,84,85,88],{},[69,86,87],{},"Custom headers",[69,89,90],{},"none",[54,92,93,96],{},[69,94,95],{},"Enabled events",[69,97,98,101],{},[43,99,100],{},"Uplink message"," only",[10,103,104],{},"Leave the other event types (join accepted, downlink queued, etc.)\ndisabled. We do not parse them.",[14,106,108],{"id":107},"payload-shape","Payload shape",[10,110,111],{},"TTN posts the standard envelope. The fields OpenSense reads:",[113,114,119],"pre",{"className":115,"code":116,"language":117,"meta":118,"style":118},"language-json shiki shiki-themes github-dark github-dark","{\n  \"end_device_ids\": {\n    \"device_id\": \"ns-t-3-04\",\n    \"application_ids\": { \"application_id\": \"opensense-cafe-bts\" },\n    \"dev_eui\": \"70B3D57ED0049ABC\"\n  },\n  \"received_at\": \"2026-05-17T08:22:00.123456789Z\",\n  \"uplink_message\": {\n    \"f_port\": 1,\n    \"decoded_payload\": {\n      \"measurements\": [\n        { \"type\": \"temperature\", \"value\": 4.13 },\n        { \"type\": \"humidity\",    \"value\": 67.8 }\n      ],\n      \"battery\": 95\n    },\n    \"rx_metadata\": [ { \"rssi\": -97, \"snr\": 7.5 } ]\n  }\n}\n","json","",[43,120,121,130,140,156,176,187,193,206,214,227,235,244,271,296,302,313,319,349,355],{"__ignoreMap":118},[122,123,126],"span",{"class":124,"line":125},"line",1,[122,127,129],{"class":128},"suv1-","{\n",[122,131,133,137],{"class":124,"line":132},2,[122,134,136],{"class":135},"s8ozJ","  \"end_device_ids\"",[122,138,139],{"class":128},": {\n",[122,141,143,146,149,153],{"class":124,"line":142},3,[122,144,145],{"class":135},"    \"device_id\"",[122,147,148],{"class":128},": ",[122,150,152],{"class":151},"s4wv1","\"ns-t-3-04\"",[122,154,155],{"class":128},",\n",[122,157,159,162,165,168,170,173],{"class":124,"line":158},4,[122,160,161],{"class":135},"    \"application_ids\"",[122,163,164],{"class":128},": { ",[122,166,167],{"class":135},"\"application_id\"",[122,169,148],{"class":128},[122,171,172],{"class":151},"\"opensense-cafe-bts\"",[122,174,175],{"class":128}," },\n",[122,177,179,182,184],{"class":124,"line":178},5,[122,180,181],{"class":135},"    \"dev_eui\"",[122,183,148],{"class":128},[122,185,186],{"class":151},"\"70B3D57ED0049ABC\"\n",[122,188,190],{"class":124,"line":189},6,[122,191,192],{"class":128},"  },\n",[122,194,196,199,201,204],{"class":124,"line":195},7,[122,197,198],{"class":135},"  \"received_at\"",[122,200,148],{"class":128},[122,202,203],{"class":151},"\"2026-05-17T08:22:00.123456789Z\"",[122,205,155],{"class":128},[122,207,209,212],{"class":124,"line":208},8,[122,210,211],{"class":135},"  \"uplink_message\"",[122,213,139],{"class":128},[122,215,217,220,222,225],{"class":124,"line":216},9,[122,218,219],{"class":135},"    \"f_port\"",[122,221,148],{"class":128},[122,223,224],{"class":135},"1",[122,226,155],{"class":128},[122,228,230,233],{"class":124,"line":229},10,[122,231,232],{"class":135},"    \"decoded_payload\"",[122,234,139],{"class":128},[122,236,238,241],{"class":124,"line":237},11,[122,239,240],{"class":135},"      \"measurements\"",[122,242,243],{"class":128},": [\n",[122,245,247,250,253,255,258,261,264,266,269],{"class":124,"line":246},12,[122,248,249],{"class":128},"        { ",[122,251,252],{"class":135},"\"type\"",[122,254,148],{"class":128},[122,256,257],{"class":151},"\"temperature\"",[122,259,260],{"class":128},", ",[122,262,263],{"class":135},"\"value\"",[122,265,148],{"class":128},[122,267,268],{"class":135},"4.13",[122,270,175],{"class":128},[122,272,274,276,278,280,283,286,288,290,293],{"class":124,"line":273},13,[122,275,249],{"class":128},[122,277,252],{"class":135},[122,279,148],{"class":128},[122,281,282],{"class":151},"\"humidity\"",[122,284,285],{"class":128},",    ",[122,287,263],{"class":135},[122,289,148],{"class":128},[122,291,292],{"class":135},"67.8",[122,294,295],{"class":128}," }\n",[122,297,299],{"class":124,"line":298},14,[122,300,301],{"class":128},"      ],\n",[122,303,305,308,310],{"class":124,"line":304},15,[122,306,307],{"class":135},"      \"battery\"",[122,309,148],{"class":128},[122,311,312],{"class":135},"95\n",[122,314,316],{"class":124,"line":315},16,[122,317,318],{"class":128},"    },\n",[122,320,322,325,328,331,333,336,338,341,343,346],{"class":124,"line":321},17,[122,323,324],{"class":135},"    \"rx_metadata\"",[122,326,327],{"class":128},": [ { ",[122,329,330],{"class":135},"\"rssi\"",[122,332,148],{"class":128},[122,334,335],{"class":135},"-97",[122,337,260],{"class":128},[122,339,340],{"class":135},"\"snr\"",[122,342,148],{"class":128},[122,344,345],{"class":135},"7.5",[122,347,348],{"class":128}," } ]\n",[122,350,352],{"class":124,"line":351},18,[122,353,354],{"class":128},"  }\n",[122,356,358],{"class":124,"line":357},19,[122,359,360],{"class":128},"}\n",[10,362,363],{},"The fields that matter:",[365,366,367,378,389,406],"ul",{},[368,369,370,373,374,377],"li",{},[43,371,372],{},"end_device_ids.dev_eui"," — primary key for device matching. This must\nbe set up in OpenSense before the first uplink, or the message lands\nin the ",[43,375,376],{},"unassigned"," tray.",[368,379,380,383,384,388],{},[43,381,382],{},"uplink_message.decoded_payload"," — the JSON your TTN payload decoder\nemitted. Must conform to the\n",[28,385,387],{"href":386},"\u002Fhardware\u002Fgeneric-lorawan","generic LoRaWAN"," shape.",[368,390,391,394,395,398,399,402,403,405],{},[43,392,393],{},"uplink_message.rx_metadata[0].rssi"," and ",[43,396,397],{},"snr"," — stored as ",[43,400,401],{},"signal","\nand ",[43,404,397],{}," auxiliary channels for diagnostics.",[368,407,408,411,412,416,417,419,420,422],{},[43,409,410],{},"received_at"," — ingest timestamp. We do ",[413,414,415],"strong",{},"not"," use TTN's ",[43,418,410],{},"\nas the measurement timestamp; the gateway clock can be wrong. The\nmeasurement uses our server's receive time, which is generally\nwithin 1–3 s of ",[43,421,410],{},".",[14,424,426],{"id":425},"authentication","Authentication",[10,428,429,430,433],{},"The webhook URL contains a long random token (",[43,431,432],{},"whk_…","). Treat it as a\npassword.",[10,435,436,437,440,441,444],{},"For extra safety, configure TTN to send a constant ",[413,438,439],{},"custom header","\n(e.g. ",[43,442,443],{},"X-OpenSense-Account-Check: \u003Crandom>",") and ask us to enforce it\nserver-side. This makes a leaked URL useless without the matching\nheader.",[14,446,448],{"id":447},"retries-and-ordering","Retries and ordering",[365,450,451,454],{},[368,452,453],{},"TTN retries 5xx for up to 24 h with exponential backoff. We return\n5xx only on infrastructure failure; we do not 5xx on payload errors.",[368,455,456,457,459,460,463],{},"TTN does ",[413,458,415],{}," preserve ordering across retries. A late delivery\nmay arrive after a newer reading. OpenSense's storage is by ",[43,461,462],{},"ts",",\nnot arrival order, so this is fine — but be aware that the dashboard's\n\"last known reading\" may briefly flip backwards in this race.",[14,465,467],{"id":466},"duty-cycle-and-cadence","Duty cycle and cadence",[10,469,470],{},"EU868 limits each device to ~1 % duty cycle. Practical cadences:",[48,472,473,486],{},[51,474,475],{},[54,476,477,480,483],{},[57,478,479],{},"Cadence",[57,481,482],{},"Devices per gateway (rough)",[57,484,485],{},"Use case",[64,487,488,499,510,521],{},[54,489,490,493,496],{},[69,491,492],{},"30 min",[69,494,495],{},"200+",[69,497,498],{},"Climate, slow drift",[54,500,501,504,507],{},[69,502,503],{},"15 min",[69,505,506],{},"80–150",[69,508,509],{},"Cold-chain default",[54,511,512,515,518],{},[69,513,514],{},"5 min",[69,516,517],{},"30–60",[69,519,520],{},"Legionella supply temp",[54,522,523,526,529],{},[69,524,525],{},"\u003C 5 min",[69,527,528],{},"not recommended in EU868",[69,530,531],{},"Hit the duty-cycle wall",[10,533,534],{},"LoRaWAN is not a substitute for WiFi if you want faster than 5 minute\ncadence. Use the right transport for the speed you need.",[14,536,538],{"id":537},"errors","Errors",[365,540,541,550,556],{},[368,542,543,546,547,549],{},[413,544,545],{},"Unassigned DevEUI"," — uplinks accepted but flagged. Visible in the\n",[43,548,376],{}," tray; one click associates them.",[368,551,552,555],{},[413,553,554],{},"Decoded payload missing"," — TTN forgot to apply the decoder. The\nmessage is stored as-is with no measurements; we do not synthesise.",[368,557,558,561],{},[413,559,560],{},"Token invalid"," — 401 to TTN; TTN gives up after retries; the\nwebhook stops sending.",[563,564,565],"style",{},"html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":118,"searchDepth":142,"depth":142,"links":567},[568,569,570,571,572,573,574],{"id":16,"depth":132,"text":17},{"id":38,"depth":132,"text":39},{"id":107,"depth":132,"text":108},{"id":425,"depth":132,"text":426},{"id":447,"depth":132,"text":448},{"id":466,"depth":132,"text":467},{"id":537,"depth":132,"text":538},"The Things Network webhook contract",null,"md",{},true,"\u002Fapi\u002Fingest-lorawan",{"title":5,"description":575},"api\u002Fingest-lorawan",240,"aEVxC-FsQ1VftbF1RVryySn9JmYDN1hqVvY_ogkPQQA",1779022954030]