[{"data":1,"prerenderedAt":726},["ShallowReactive",2],{"doc-\u002Fapi\u002Foverview":3},{"id":4,"title":5,"body":6,"description":716,"edit":717,"extension":718,"meta":719,"navigation":720,"path":721,"seo":722,"stem":723,"vertical":717,"weight":724,"__hash__":725},"content\u002Fapi\u002Foverview.md","API overview",{"type":7,"value":8,"toc":705},"minimark",[9,13,18,30,47,51,58,132,143,150,154,157,238,251,391,395,416,435,457,461,464,541,556,560,567,586,589,639,652,656,675,679,694,698,701],[10,11,12],"p",{},"The OpenSense HTTP API is a small REST surface plus a single ingest endpoint\nand a few non-REST helpers. Everything is JSON over HTTPS. There is one\nproduction base URL and no staging environment.",[14,15,17],"h2",{"id":16},"base-url","Base URL",[19,20,22],"cmd",{"label":21},"$ base url",[10,23,24],{},[25,26,27],"a",{"href":27,"rel":28},"https:\u002F\u002Fapi.opensense.murzin.digital",[29],"nofollow",[10,31,32,33,37,38,42,43,46],{},"All endpoints are versioned under ",[34,35,36],"code",{},"\u002Fv1",". The version is ",[39,40,41],"strong",{},"major version\nonly",". Backwards-compatible changes go inside v1; breaking changes ship as\n",[34,44,45],{},"\u002Fv2"," with a 12-month overlap.",[14,48,50],{"id":49},"authentication","Authentication",[10,52,53,54,57],{},"Three token types, all carried in the ",[34,55,56],{},"Authorization: Bearer \u003Ctoken>"," header\nunless otherwise noted.",[59,60,61,80],"table",{},[62,63,64],"thead",{},[65,66,67,71,74,77],"tr",{},[68,69,70],"th",{},"Token",[68,72,73],{},"Prefix",[68,75,76],{},"Scope",[68,78,79],{},"Rotation",[81,82,83,100,116],"tbody",{},[65,84,85,89,94,97],{},[86,87,88],"td",{},"Device token",[86,90,91],{},[34,92,93],{},"ds_live_",[86,95,96],{},"one device's ingest endpoints",[86,98,99],{},"per device, any time",[65,101,102,105,110,113],{},[86,103,104],{},"User API token",[86,106,107],{},[34,108,109],{},"ua_live_",[86,111,112],{},"management endpoints (CRUD)",[86,114,115],{},"per user, any time",[65,117,118,121,126,129],{},[86,119,120],{},"Webhook secret",[86,122,123],{},[34,124,125],{},"whk_",[86,127,128],{},"inbound from Shelly \u002F TTN URLs",[86,130,131],{},"per integration",[10,133,134,135,138,139,142],{},"Device tokens are issued by the dashboard when you add a device. They can be\nembedded in a query string (",[34,136,137],{},"?token=…",") for the legacy Shelly path; everywhere\nelse, use the ",[34,140,141],{},"Authorization"," header.",[10,144,145,146,149],{},"Magic-link auth is the ",[39,147,148],{},"web UI's"," auth, not the API's. The web UI\nexchanges the magic-link code for a session cookie; the cookie is not\nintended for use against the API.",[14,151,153],{"id":152},"errors","Errors",[10,155,156],{},"All errors are JSON with a stable shape:",[158,159,164],"pre",{"className":160,"code":161,"language":162,"meta":163,"style":163},"language-json shiki shiki-themes github-dark github-dark","{\n  \"error\": {\n    \"code\":   \"channel_not_found\",\n    \"message\": \"No channel with id chn_4f3c... in this account\",\n    \"request_id\": \"req_b1d2e3f4\"\n  }\n}\n","json","",[34,165,166,175,185,201,215,226,232],{"__ignoreMap":163},[167,168,171],"span",{"class":169,"line":170},"line",1,[167,172,174],{"class":173},"suv1-","{\n",[167,176,178,182],{"class":169,"line":177},2,[167,179,181],{"class":180},"s8ozJ","  \"error\"",[167,183,184],{"class":173},": {\n",[167,186,188,191,194,198],{"class":169,"line":187},3,[167,189,190],{"class":180},"    \"code\"",[167,192,193],{"class":173},":   ",[167,195,197],{"class":196},"s4wv1","\"channel_not_found\"",[167,199,200],{"class":173},",\n",[167,202,204,207,210,213],{"class":169,"line":203},4,[167,205,206],{"class":180},"    \"message\"",[167,208,209],{"class":173},": ",[167,211,212],{"class":196},"\"No channel with id chn_4f3c... in this account\"",[167,214,200],{"class":173},[167,216,218,221,223],{"class":169,"line":217},5,[167,219,220],{"class":180},"    \"request_id\"",[167,222,209],{"class":173},[167,224,225],{"class":196},"\"req_b1d2e3f4\"\n",[167,227,229],{"class":169,"line":228},6,[167,230,231],{"class":173},"  }\n",[167,233,235],{"class":169,"line":234},7,[167,236,237],{"class":173},"}\n",[10,239,240,242,243,246,247,250],{},[34,241,34],{}," is the stable, machine-readable identifier. ",[34,244,245],{},"message"," is human and\nmay change wording. ",[34,248,249],{},"request_id"," is what you put in a support email — we\nkeep server-side logs keyed by it for 30 days.",[59,252,253,269],{},[62,254,255],{},[65,256,257,260,263],{},[68,258,259],{},"HTTP",[68,261,262],{},"Meaning",[68,264,265,266,268],{},"Common ",[34,267,34],{},"s",[81,270,271,288,304,317,333,346,362,375],{},[65,272,273,276,279],{},[86,274,275],{},"400",[86,277,278],{},"Bad request — payload invalid",[86,280,281,284,285],{},[34,282,283],{},"bad_json",", ",[34,286,287],{},"invalid_measurement",[65,289,290,293,296],{},[86,291,292],{},"401",[86,294,295],{},"Missing or wrong token",[86,297,298,284,301],{},[34,299,300],{},"auth_required",[34,302,303],{},"token_invalid",[65,305,306,309,312],{},[86,307,308],{},"403",[86,310,311],{},"Token correct, scope wrong",[86,313,314],{},[34,315,316],{},"forbidden",[65,318,319,322,325],{},[86,320,321],{},"404",[86,323,324],{},"Resource does not exist for this account",[86,326,327,284,330],{},[34,328,329],{},"device_not_found",[34,331,332],{},"channel_not_found",[65,334,335,338,341],{},[86,336,337],{},"409",[86,339,340],{},"Idempotency conflict — different body, same key",[86,342,343],{},[34,344,345],{},"idempotency_conflict",[65,347,348,351,354],{},[86,349,350],{},"422",[86,352,353],{},"Payload parsed but semantically invalid",[86,355,356,284,359],{},[34,357,358],{},"out_of_range",[34,360,361],{},"unknown_kind",[65,363,364,367,370],{},[86,365,366],{},"429",[86,368,369],{},"Rate limited",[86,371,372],{},[34,373,374],{},"rate_limited",[65,376,377,380,383],{},[86,378,379],{},"5xx",[86,381,382],{},"Server error — safe to retry with backoff",[86,384,385,284,388],{},[34,386,387],{},"internal",[34,389,390],{},"db_unavailable",[14,392,394],{"id":393},"idempotency","Idempotency",[10,396,397,398,401,402,401,405,401,408,411,412,415],{},"Mutating endpoints (everything that is ",[34,399,400],{},"POST","\u002F",[34,403,404],{},"PUT",[34,406,407],{},"PATCH",[34,409,410],{},"DELETE"," and not\nstrictly idempotent on its own) accept an ",[34,413,414],{},"Idempotency-Key"," request header.\nPick any opaque string up to 128 chars; we recommend a UUID.",[10,417,418,419,422,423,426,427,430,431,434],{},"OpenSense remembers the ",[34,420,421],{},"(account, idempotency_key)"," tuple for ",[39,424,425],{},"24 hours",".\nA repeated request with the same key and the same body returns the same\nresponse (cached). A repeated request with the same key and a ",[39,428,429],{},"different","\nbody returns ",[34,432,433],{},"409 idempotency_conflict",".",[10,436,437,438,441,442,445,446,449,450,453,454,456],{},"The ingest endpoint (",[34,439,440],{},"POST \u002Fv1\u002Fingest",") is ",[39,443,444],{},"content-idempotent",": the\ndeduplication key is ",[34,447,448],{},"(device_id, ts, channel_id)",". You do ",[39,451,452],{},"not"," need to\nsend an ",[34,455,414],{}," for ingest; sending the same measurement twice is\na no-op.",[14,458,460],{"id":459},"rate-limits","Rate limits",[10,462,463],{},"Per-account, per-route family. Limits are advisory — we will warn before\nenforcing, and the limit grows automatically with your subscription.",[59,465,466,479],{},[62,467,468],{},[65,469,470,473,476],{},[68,471,472],{},"Route family",[68,474,475],{},"Default limit",[68,477,478],{},"Header",[81,480,481,495,510,525],{},[65,482,483,487,490],{},[86,484,485],{},[34,486,440],{},[86,488,489],{},"60 req \u002F min \u002F device",[86,491,492],{},[34,493,494],{},"X-RateLimit-Device",[65,496,497,502,505],{},[86,498,499],{},[34,500,501],{},"GET  \u002Fv1\u002Fmeasurements",[86,503,504],{},"120 req \u002F min \u002F account",[86,506,507],{},[34,508,509],{},"X-RateLimit-Read",[65,511,512,517,520],{},[86,513,514],{},[34,515,516],{},"POST \u002Fv1\u002Freports",[86,518,519],{},"4 req \u002F min \u002F account",[86,521,522],{},[34,523,524],{},"X-RateLimit-Report",[65,526,527,533,536],{},[86,528,529,532],{},[34,530,531],{},"* \u002Fv1\u002F*"," (everything else)",[86,534,535],{},"600 req \u002F min \u002F account",[86,537,538],{},[34,539,540],{},"X-RateLimit-Mgmt",[10,542,543,544,547,548,551,552,555],{},"Responses include ",[34,545,546],{},"X-RateLimit-Remaining"," and ",[34,549,550],{},"X-RateLimit-Reset"," (epoch\nseconds). 429 responses include ",[34,553,554],{},"Retry-After"," (seconds).",[14,557,559],{"id":558},"pagination","Pagination",[10,561,562,563,566],{},"List endpoints use ",[39,564,565],{},"cursor pagination",". Two query params:",[568,569,570,577],"ul",{},[571,572,573,576],"li",{},[34,574,575],{},"limit",": 1–500, default 100.",[571,578,579,582,583,434],{},[34,580,581],{},"cursor",": opaque, returned in the response body as ",[34,584,585],{},"next_cursor",[10,587,588],{},"Response shape:",[158,590,592],{"className":160,"code":591,"language":162,"meta":163,"style":163},"{\n  \"data\": [ … ],\n  \"next_cursor\": \"eyJ0cyI6...\",\n  \"has_more\": true\n}\n",[34,593,594,598,613,625,635],{"__ignoreMap":163},[167,595,596],{"class":169,"line":170},[167,597,174],{"class":173},[167,599,600,603,606,610],{"class":169,"line":177},[167,601,602],{"class":180},"  \"data\"",[167,604,605],{"class":173},": [ ",[167,607,609],{"class":608},"sX7ps","…",[167,611,612],{"class":173}," ],\n",[167,614,615,618,620,623],{"class":169,"line":187},[167,616,617],{"class":180},"  \"next_cursor\"",[167,619,209],{"class":173},[167,621,622],{"class":196},"\"eyJ0cyI6...\"",[167,624,200],{"class":173},[167,626,627,630,632],{"class":169,"line":203},[167,628,629],{"class":180},"  \"has_more\"",[167,631,209],{"class":173},[167,633,634],{"class":180},"true\n",[167,636,637],{"class":169,"line":217},[167,638,237],{"class":173},[10,640,641,642,645,646,284,649,651],{},"When ",[34,643,644],{},"has_more"," is ",[34,647,648],{},"false",[34,650,585],{}," is null.",[14,653,655],{"id":654},"timestamps","Timestamps",[568,657,658,665,672],{},[571,659,660,661,664],{},"All timestamps are RFC 3339 with explicit timezone offset, always UTC\n(",[34,662,663],{},"Z",").",[571,666,667,668,671],{},"Resolution: microseconds for measurement ",[34,669,670],{},"ts",", seconds elsewhere.",[571,673,674],{},"The server tolerates RFC 3339 with non-UTC offsets on input and converts.",[14,676,678],{"id":677},"content-negotiation","Content negotiation",[10,680,681,682,685,686,689,690,693],{},"We accept and emit ",[34,683,684],{},"application\u002Fjson",". The reports endpoint additionally\nemits ",[34,687,688],{},"application\u002Fpdf",". ",[34,691,692],{},"Accept: application\u002Fjson"," is the default; no other\nformats are negotiated.",[14,695,697],{"id":696},"sdks","SDKs",[10,699,700],{},"There is no first-party SDK yet. The HTTP surface is small enough that hand-\nwritten code is fine. See the language tabs on each endpoint page for\ncopy-paste recipes in Python, Go, ESPHome, and Arduino.",[702,703,704],"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);}html pre.shiki code .sX7ps, html code.shiki .sX7ps{--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":163,"searchDepth":187,"depth":187,"links":706},[707,708,709,710,711,712,713,714,715],{"id":16,"depth":177,"text":17},{"id":49,"depth":177,"text":50},{"id":152,"depth":177,"text":153},{"id":393,"depth":177,"text":394},{"id":459,"depth":177,"text":460},{"id":558,"depth":177,"text":559},{"id":654,"depth":177,"text":655},{"id":677,"depth":177,"text":678},{"id":696,"depth":177,"text":697},"Base URL, auth, errors, idempotency, rate limits",null,"md",{},true,"\u002Fapi\u002Foverview",{"title":5,"description":716},"api\u002Foverview",200,"x2BB1y5xwEsM5xzBUE5SnbKGNw9CaRlIIvw9fJf9T90",1779022953879]