Webhooks
POST signed payloads to your incident system
Webhooks are the integration path for customers running their own incident system (PagerDuty, Opsgenie, BetterStack, a homemade Sinatra app). OpenSense POSTs a JSON payload on each event; you do whatever you want with it.
Setup
Account → Integrations → Webhooks → + Add:
- URL: where we POST.
- Events: which event kinds (alarm_opened, alarm_ack, alarm_cleared, device_offline, device_online, silence_opened, silence_closed, report_ready).
- Secret: a long random string. We sign every payload with this.
One webhook destination per Solo subscription. Team tier raises to unlimited.
Payload
{
"event": {
"id": "evt_4f3c",
"kind": "alarm_opened",
"severity": "alarm",
"opened_at": "2026-05-17T08:10:00Z",
"site_id": "site_4f3c",
"device_id": "dev_b1d2",
"channel_id":"chn_a1b2",
"value_at": 11.2,
"thresholds": { "ok_min": -2, "ok_max": 8 }
},
"delivery": {
"id": "wh_4f3c1a",
"attempt": 1,
"delivered_at": "2026-05-17T08:10:01Z"
}
}
Fields and shapes match the events API.
Signature
Each request carries:
X-OpenSense-Signature:t=1717248000,v1=base64(HMAC-SHA256(secret, t + "." + body))X-OpenSense-Delivery-Id: a unique id for retry deduplication.X-OpenSense-Timestamp: epoch seconds (same astabove).
Verify:
import hmac, hashlib, base64, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
sig = headers["X-OpenSense-Signature"]
parts = dict(p.split("=", 1) for p in sig.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > 300:
return False
expected = base64.b64encode(
hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(v1, expected)
The 5-minute timestamp tolerance prevents replay; we reject on our side if our clock disagrees by more than 5 min with NTP.
Retry policy
| Result | Retry? |
|---|---|
| 2xx | Done |
| 3xx | Follow up to 3 redirects |
| 4xx (non-429) | Drop. Log to Outbound. No retry. |
| 429 | Respect Retry-After. Re-queue. |
| 5xx | Exponential backoff: 5 s, 30 s, 5 min, 30 min, 1 h, 6 h. Then drop. |
| Timeout (> 5 s) | Counts as 5xx |
Drops are visible in the dashboard's Outbound log with the
response body (first 4 KB). Investigate from there.
Idempotency
The X-OpenSense-Delivery-Id header lets your endpoint deduplicate.
A retry of the same delivery uses the same id. Different events
have different ids; an event acknowledged on retry is still a
different event from the original alarm_opened.
What to do with the payload
For PagerDuty / Opsgenie: translate to their Events v2 format. The mapping is straightforward:
alarm_opened→triggeralarm_ack→acknowledgealarm_cleared→resolve
We will publish per-vendor adapter snippets on GitHub once the client base for each grows past a handful.
For homemade systems: idempotency by delivery_id, signature
verification, then whatever business logic.
What you give up
- No custom payload shape. The schema is the schema. Use a transform proxy if you need a different shape.
- Single webhook per site. Team tier raises to multiple per site.
- No mutual TLS. Endpoint is plain HTTPS with bearer-style HMAC. mTLS may come later if customers ask.