Webhooks
Webhook Receivers
Build an endpoint that receives Rhombus webhooks safely: verify signatures on the raw body, answer quickly and queue the work, reach systems on a private network without opening it to the internet, and test locally.
On this page
A webhook receiver is the HTTPS endpoint Rhombus posts events to. This guide builds one that verifies every request, answers within Rhombus's time limits, and hands the work to the rest of your systems, including systems on a private network that can't accept connections from the internet.
Before you start, read Webhooks for the two kinds of webhook (a rule's webhook action and the organization's Activity and Diagnostic webhooks), their payloads and their delivery rules.
What the receiver must do
- Accept HTTPS at a public address with a certificate from a public certificate authority.
- Verify the signature over the raw request body before trusting anything in it.
- Answer
200within a few seconds. Rhombus allows 2 seconds to connect for rule webhooks and makes one attempt; slow work belongs after the response. - Queue the event for your workers, and let them de-duplicate by the IDs in the payload.
A receiver in Node.js and Python
Both examples verify x-rhombus-signature-sha256, the lowercase-hex HMAC-SHA256 of the raw body keyed with the webhook's secret. Rule webhook actions and organization webhooks both send it, so the same receiver handles either kind; organization webhooks also send x-rhombus-signature-sha1, which you don't need. The signature covers the exact bytes Rhombus sent, so read the body raw: a JSON parser that runs first (such as express.json()) changes it, and the check fails.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.RHOMBUS_WEBHOOK_SECRET; // the webhook's secret
const app = express();
// express.raw keeps the body as bytes so the signature can be checked.
app.post("/rhombus/webhook", express.raw({ type: "*/*", limit: "1mb" }), (req, res) => {
const sent = Buffer.from(req.get("x-rhombus-signature-sha256") ?? "");
const expected = Buffer.from(crypto.createHmac("sha256", SECRET).update(req.body).digest("hex"));
if (sent.length !== expected.length || !crypto.timingSafeEqual(sent, expected)) { // constant time
return res.sendStatus(401);
}
res.sendStatus(200); // answer first
queue.push(JSON.parse(req.body.toString("utf8"))); // then hand off: SQS, Pub/Sub, Kafka, a table
});
app.listen(8080);import hashlib
import hmac
import json
import os
from flask import Flask, abort, request
SECRET = os.environ["RHOMBUS_WEBHOOK_SECRET"].encode() # the webhook's secret
app = Flask(__name__)
@app.post("/rhombus/webhook")
def rhombus_webhook():
body = request.get_data() # the raw bytes the signature covers
sent = request.headers.get("x-rhombus-signature-sha256", "")
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sent, expected): # constant time
abort(401)
queue.put(json.loads(body)) # SQS, Pub/Sub, Kafka, a table
return "", 200A rule webhook's body describes the rule that fired and the events that matched it:
const { ruleUuid, triggeredTimestampMs, deviceEvents = [] } = event;
for (const deviceEvent of deviceEvents) {
console.log(ruleUuid, deviceEvent.deviceUuid, deviceEvent.activities, deviceEvent.timestampMs);
}Webhooks shows both payloads in full.
Where to run it
| Situation | Run the receiver | Notes |
|---|---|---|
| Your systems are in the cloud | A container or function behind your load balancer or API gateway | The simplest option |
| Your systems are on a private network | A small public relay (a serverless function plus a queue) that verifies and queues; private workers pull from the queue | No inbound connection into the private network |
| You're developing locally | Your laptop, behind a temporary tunnel from a tunneling service | For testing only; close the tunnel when you're done |
Private networks: relay through a queue
Systems that can't accept connections from the internet still reach out. Put only the receiver on the internet, and let your private systems collect the events:
- The receiver holds only the webhook secret, never an API key.
- The workers need outbound access to the queue and nothing inbound.
- If the queue or the workers are down, events wait in the queue; if the receiver is down, Rhombus doesn't resend, so reconcile from the API.
Avoid forwarding webhooks into the private network over an SSH tunnel or a port forward: it exposes an inbound path to an internal host, and the tunnel becomes a credential and a single point of failure to maintain.
Local testing
To try a receiver on your machine, expose its port through a tunneling service, add the tunnel's HTTPS URL as the webhook URL, and trigger the rule. Remove the webhook from Rhombus and close the tunnel when you're done.
Configure Rhombus
- Add the webhook
For a rule's webhook action, open the rule in the console, add a webhook action with your receiver's HTTPS URL, and save. For an organization webhook, go to Settings → Integrations & Developer Resources → Webhooks, add the URL and choose Activity or Diagnostic.
- Store the secret
Copy the webhook secret into your receiver's configuration, from a secrets manager rather than source code. Find your secret shows where each kind keeps it.
- Trigger a test event
Cause an event the rule or webhook covers, and confirm the receiver logs a verified request and returns
200.
Run it in production
- Monitor the receiver: alert on non-
2xxanswers, signature failures and slow responses. A failed rule webhook delivery also appears in Rhombus as anEXTERNAL_WEBHOOK_FAILUREdiagnostic event. - Reject unsigned requests. A request without a valid signature didn't come from Rhombus.
- Keep payloads out of shared logs. They name people, places and devices; log the IDs you need instead.
- Replace a leaked secret: delete the webhook and create it again, which gives it a new secret, then update the receiver.