[{"data":1,"prerenderedAt":753},["ShallowReactive",2],{"doc-\u002Fintegrations\u002Fwebhooks":3},{"id":4,"title":5,"body":6,"description":744,"edit":745,"extension":746,"meta":747,"navigation":353,"path":748,"seo":749,"stem":750,"vertical":745,"weight":751,"__hash__":752},"content\u002Fintegrations\u002Fwebhooks.md","Webhooks",{"type":7,"value":8,"toc":735},"minimark",[9,13,18,25,48,51,55,288,297,301,304,330,333,560,563,567,643,649,653,662,666,669,697,700,707,711,731],[10,11,12],"p",{},"Webhooks are the integration path for customers running their own\nincident system (PagerDuty, Opsgenie, BetterStack, a homemade\nSinatra app). OpenSense POSTs a JSON payload on each event; you do\nwhatever you want with it.",[14,15,17],"h2",{"id":16},"setup","Setup",[10,19,20,24],{},[21,22,23],"code",{},"Account → Integrations → Webhooks → + Add",":",[26,27,28,36,42],"ul",{},[29,30,31,35],"li",{},[32,33,34],"strong",{},"URL",": where we POST.",[29,37,38,41],{},[32,39,40],{},"Events",": which event kinds (alarm_opened, alarm_ack,\nalarm_cleared, device_offline, device_online, silence_opened,\nsilence_closed, report_ready).",[29,43,44,47],{},[32,45,46],{},"Secret",": a long random string. We sign every payload with this.",[10,49,50],{},"One webhook destination per Solo subscription. Team tier raises to\nunlimited.",[14,52,54],{"id":53},"payload","Payload",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-json shiki shiki-themes github-dark github-dark","{\n  \"event\": {\n    \"id\":        \"evt_4f3c\",\n    \"kind\":      \"alarm_opened\",\n    \"severity\":  \"alarm\",\n    \"opened_at\": \"2026-05-17T08:10:00Z\",\n    \"site_id\":   \"site_4f3c\",\n    \"device_id\": \"dev_b1d2\",\n    \"channel_id\":\"chn_a1b2\",\n    \"value_at\":   11.2,\n    \"thresholds\": { \"ok_min\": -2, \"ok_max\": 8 }\n  },\n  \"delivery\": {\n    \"id\":        \"wh_4f3c1a\",\n    \"attempt\":    1,\n    \"delivered_at\": \"2026-05-17T08:10:01Z\"\n  }\n}\n","json","",[21,63,64,73,83,99,113,127,141,155,168,181,194,225,231,239,251,265,276,282],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"suv1-","{\n",[65,74,76,80],{"class":67,"line":75},2,[65,77,79],{"class":78},"s8ozJ","  \"event\"",[65,81,82],{"class":71},": {\n",[65,84,86,89,92,96],{"class":67,"line":85},3,[65,87,88],{"class":78},"    \"id\"",[65,90,91],{"class":71},":        ",[65,93,95],{"class":94},"s4wv1","\"evt_4f3c\"",[65,97,98],{"class":71},",\n",[65,100,102,105,108,111],{"class":67,"line":101},4,[65,103,104],{"class":78},"    \"kind\"",[65,106,107],{"class":71},":      ",[65,109,110],{"class":94},"\"alarm_opened\"",[65,112,98],{"class":71},[65,114,116,119,122,125],{"class":67,"line":115},5,[65,117,118],{"class":78},"    \"severity\"",[65,120,121],{"class":71},":  ",[65,123,124],{"class":94},"\"alarm\"",[65,126,98],{"class":71},[65,128,130,133,136,139],{"class":67,"line":129},6,[65,131,132],{"class":78},"    \"opened_at\"",[65,134,135],{"class":71},": ",[65,137,138],{"class":94},"\"2026-05-17T08:10:00Z\"",[65,140,98],{"class":71},[65,142,144,147,150,153],{"class":67,"line":143},7,[65,145,146],{"class":78},"    \"site_id\"",[65,148,149],{"class":71},":   ",[65,151,152],{"class":94},"\"site_4f3c\"",[65,154,98],{"class":71},[65,156,158,161,163,166],{"class":67,"line":157},8,[65,159,160],{"class":78},"    \"device_id\"",[65,162,135],{"class":71},[65,164,165],{"class":94},"\"dev_b1d2\"",[65,167,98],{"class":71},[65,169,171,174,176,179],{"class":67,"line":170},9,[65,172,173],{"class":78},"    \"channel_id\"",[65,175,24],{"class":71},[65,177,178],{"class":94},"\"chn_a1b2\"",[65,180,98],{"class":71},[65,182,184,187,189,192],{"class":67,"line":183},10,[65,185,186],{"class":78},"    \"value_at\"",[65,188,149],{"class":71},[65,190,191],{"class":78},"11.2",[65,193,98],{"class":71},[65,195,197,200,203,206,208,211,214,217,219,222],{"class":67,"line":196},11,[65,198,199],{"class":78},"    \"thresholds\"",[65,201,202],{"class":71},": { ",[65,204,205],{"class":78},"\"ok_min\"",[65,207,135],{"class":71},[65,209,210],{"class":78},"-2",[65,212,213],{"class":71},", ",[65,215,216],{"class":78},"\"ok_max\"",[65,218,135],{"class":71},[65,220,221],{"class":78},"8",[65,223,224],{"class":71}," }\n",[65,226,228],{"class":67,"line":227},12,[65,229,230],{"class":71},"  },\n",[65,232,234,237],{"class":67,"line":233},13,[65,235,236],{"class":78},"  \"delivery\"",[65,238,82],{"class":71},[65,240,242,244,246,249],{"class":67,"line":241},14,[65,243,88],{"class":78},[65,245,91],{"class":71},[65,247,248],{"class":94},"\"wh_4f3c1a\"",[65,250,98],{"class":71},[65,252,254,257,260,263],{"class":67,"line":253},15,[65,255,256],{"class":78},"    \"attempt\"",[65,258,259],{"class":71},":    ",[65,261,262],{"class":78},"1",[65,264,98],{"class":71},[65,266,268,271,273],{"class":67,"line":267},16,[65,269,270],{"class":78},"    \"delivered_at\"",[65,272,135],{"class":71},[65,274,275],{"class":94},"\"2026-05-17T08:10:01Z\"\n",[65,277,279],{"class":67,"line":278},17,[65,280,281],{"class":71},"  }\n",[65,283,285],{"class":67,"line":284},18,[65,286,287],{"class":71},"}\n",[10,289,290,291,296],{},"Fields and shapes match the ",[292,293,295],"a",{"href":294},"\u002Fapi\u002Falerts#events","events API",".",[14,298,300],{"id":299},"signature","Signature",[10,302,303],{},"Each request carries:",[26,305,306,314,320],{},[29,307,308,135,311],{},[21,309,310],{},"X-OpenSense-Signature",[21,312,313],{},"t=1717248000,v1=base64(HMAC-SHA256(secret, t + \".\" + body))",[29,315,316,319],{},[21,317,318],{},"X-OpenSense-Delivery-Id",": a unique id for retry deduplication.",[29,321,322,325,326,329],{},[21,323,324],{},"X-OpenSense-Timestamp",": epoch seconds (same as ",[21,327,328],{},"t"," above).",[10,331,332],{},"Verify:",[56,334,338],{"className":335,"code":336,"language":337,"meta":61,"style":61},"language-python shiki shiki-themes github-dark github-dark","import hmac, hashlib, base64, time\n\ndef verify(secret: str, headers: dict, body: bytes) -> bool:\n    sig = headers[\"X-OpenSense-Signature\"]\n    parts = dict(p.split(\"=\", 1) for p in sig.split(\",\"))\n    t, v1 = parts[\"t\"], parts[\"v1\"]\n    if abs(time.time() - int(t)) > 300:\n        return False\n    expected = base64.b64encode(\n        hmac.new(secret.encode(), f\"{t}.\".encode() + body, hashlib.sha256).digest()\n    ).decode()\n    return hmac.compare_digest(v1, expected)\n","python",[21,339,340,349,355,391,408,449,470,498,506,516,547,552],{"__ignoreMap":61},[65,341,342,346],{"class":67,"line":68},[65,343,345],{"class":344},"sOPea","import",[65,347,348],{"class":71}," hmac, hashlib, base64, time\n",[65,350,351],{"class":67,"line":75},[65,352,354],{"emptyLinePlaceholder":353},true,"\n",[65,356,357,360,364,367,370,373,376,379,382,385,388],{"class":67,"line":85},[65,358,359],{"class":344},"def",[65,361,363],{"class":362},"sFR8T"," verify",[65,365,366],{"class":71},"(secret: ",[65,368,369],{"class":78},"str",[65,371,372],{"class":71},", headers: ",[65,374,375],{"class":78},"dict",[65,377,378],{"class":71},", body: ",[65,380,381],{"class":78},"bytes",[65,383,384],{"class":71},") -> ",[65,386,387],{"class":78},"bool",[65,389,390],{"class":71},":\n",[65,392,393,396,399,402,405],{"class":67,"line":101},[65,394,395],{"class":71},"    sig ",[65,397,398],{"class":344},"=",[65,400,401],{"class":71}," headers[",[65,403,404],{"class":94},"\"X-OpenSense-Signature\"",[65,406,407],{"class":71},"]\n",[65,409,410,413,415,418,421,424,426,428,431,434,437,440,443,446],{"class":67,"line":115},[65,411,412],{"class":71},"    parts ",[65,414,398],{"class":344},[65,416,417],{"class":78}," dict",[65,419,420],{"class":71},"(p.split(",[65,422,423],{"class":94},"\"=\"",[65,425,213],{"class":71},[65,427,262],{"class":78},[65,429,430],{"class":71},") ",[65,432,433],{"class":344},"for",[65,435,436],{"class":71}," p ",[65,438,439],{"class":344},"in",[65,441,442],{"class":71}," sig.split(",[65,444,445],{"class":94},"\",\"",[65,447,448],{"class":71},"))\n",[65,450,451,454,456,459,462,465,468],{"class":67,"line":129},[65,452,453],{"class":71},"    t, v1 ",[65,455,398],{"class":344},[65,457,458],{"class":71}," parts[",[65,460,461],{"class":94},"\"t\"",[65,463,464],{"class":71},"], parts[",[65,466,467],{"class":94},"\"v1\"",[65,469,407],{"class":71},[65,471,472,475,478,481,484,487,490,493,496],{"class":67,"line":143},[65,473,474],{"class":344},"    if",[65,476,477],{"class":78}," abs",[65,479,480],{"class":71},"(time.time() ",[65,482,483],{"class":344},"-",[65,485,486],{"class":78}," int",[65,488,489],{"class":71},"(t)) ",[65,491,492],{"class":344},">",[65,494,495],{"class":78}," 300",[65,497,390],{"class":71},[65,499,500,503],{"class":67,"line":157},[65,501,502],{"class":344},"        return",[65,504,505],{"class":78}," False\n",[65,507,508,511,513],{"class":67,"line":170},[65,509,510],{"class":71},"    expected ",[65,512,398],{"class":344},[65,514,515],{"class":71}," base64.b64encode(\n",[65,517,518,521,524,527,530,532,535,538,541,544],{"class":67,"line":183},[65,519,520],{"class":71},"        hmac.new(secret.encode(), ",[65,522,523],{"class":344},"f",[65,525,526],{"class":94},"\"",[65,528,529],{"class":78},"{",[65,531,328],{"class":71},[65,533,534],{"class":78},"}",[65,536,537],{"class":94},".\"",[65,539,540],{"class":71},".encode() ",[65,542,543],{"class":344},"+",[65,545,546],{"class":71}," body, hashlib.sha256).digest()\n",[65,548,549],{"class":67,"line":196},[65,550,551],{"class":71},"    ).decode()\n",[65,553,554,557],{"class":67,"line":227},[65,555,556],{"class":344},"    return",[65,558,559],{"class":71}," hmac.compare_digest(v1, expected)\n",[10,561,562],{},"The 5-minute timestamp tolerance prevents replay; we reject on our\nside if our clock disagrees by more than 5 min with NTP.",[14,564,566],{"id":565},"retry-policy","Retry policy",[568,569,570,583],"table",{},[571,572,573],"thead",{},[574,575,576,580],"tr",{},[577,578,579],"th",{},"Result",[577,581,582],{},"Retry?",[584,585,586,595,603,615,627,635],"tbody",{},[574,587,588,592],{},[589,590,591],"td",{},"2xx",[589,593,594],{},"Done",[574,596,597,600],{},[589,598,599],{},"3xx",[589,601,602],{},"Follow up to 3 redirects",[574,604,605,608],{},[589,606,607],{},"4xx (non-429)",[589,609,610,611,614],{},"Drop. Log to ",[21,612,613],{},"Outbound",". No retry.",[574,616,617,620],{},[589,618,619],{},"429",[589,621,622,623,626],{},"Respect ",[21,624,625],{},"Retry-After",". Re-queue.",[574,628,629,632],{},[589,630,631],{},"5xx",[589,633,634],{},"Exponential backoff: 5 s, 30 s, 5 min, 30 min, 1 h, 6 h. Then drop.",[574,636,637,640],{},[589,638,639],{},"Timeout (> 5 s)",[589,641,642],{},"Counts as 5xx",[10,644,645,646,648],{},"Drops are visible in the dashboard's ",[21,647,613],{}," log with the\nresponse body (first 4 KB). Investigate from there.",[14,650,652],{"id":651},"idempotency","Idempotency",[10,654,655,656,658,659,296],{},"The ",[21,657,318],{}," header lets your endpoint deduplicate.\nA retry of the same delivery uses the same id. Different events\nhave different ids; an event acknowledged on retry is still a\ndifferent event from the original ",[21,660,661],{},"alarm_opened",[14,663,665],{"id":664},"what-to-do-with-the-payload","What to do with the payload",[10,667,668],{},"For PagerDuty \u002F Opsgenie: translate to their Events v2 format. The\nmapping is straightforward:",[26,670,671,679,688],{},[29,672,673,675,676],{},[21,674,661],{}," → ",[21,677,678],{},"trigger",[29,680,681,684,685],{},[21,682,683],{},"alarm_ack","    → ",[21,686,687],{},"acknowledge",[29,689,690,693,694],{},[21,691,692],{},"alarm_cleared","→ ",[21,695,696],{},"resolve",[10,698,699],{},"We will publish per-vendor adapter snippets on GitHub once the\nclient base for each grows past a handful.",[10,701,702,703,706],{},"For homemade systems: idempotency by ",[21,704,705],{},"delivery_id",", signature\nverification, then whatever business logic.",[14,708,710],{"id":709},"what-you-give-up","What you give up",[26,712,713,719,725],{},[29,714,715,718],{},[32,716,717],{},"No custom payload shape."," The schema is the schema. Use a\ntransform proxy if you need a different shape.",[29,720,721,724],{},[32,722,723],{},"Single webhook per site."," Team tier raises to multiple per\nsite.",[29,726,727,730],{},[32,728,729],{},"No mutual TLS."," Endpoint is plain HTTPS with bearer-style HMAC.\nmTLS may come later if customers ask.",[732,733,734],"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 .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}",{"title":61,"searchDepth":85,"depth":85,"links":736},[737,738,739,740,741,742,743],{"id":16,"depth":75,"text":17},{"id":53,"depth":75,"text":54},{"id":299,"depth":75,"text":300},{"id":565,"depth":75,"text":566},{"id":651,"depth":75,"text":652},{"id":664,"depth":75,"text":665},{"id":709,"depth":75,"text":710},"POST signed payloads to your incident system",null,"md",{},"\u002Fintegrations\u002Fwebhooks",{"title":5,"description":744},"integrations\u002Fwebhooks",630,"edog7FMayvKli4YwsqW9v4Vqh-Xp75QAppis-HPwTFE",1779022955222]