> ## Documentation index
> Fetch the complete documentation index at: https://developer.rhombus.com/llms.txt
> Use it to discover all available pages before exploring further.

# 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.

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](https://developer.rhombus.com/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](https://developer.rhombus.com/webhooks#delivery).

## What the receiver must do

1. **Accept HTTPS** at a public address with a certificate from a public certificate authority.
2. **Verify the signature** over the raw request body before trusting anything in it.
3. **Answer `200` within a few seconds.** Rhombus allows 2 seconds to connect for rule webhooks and makes one attempt; slow work belongs after the response.
4. **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.

```javascript Node.js (Express)
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);
```

```python Python (Flask)
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 "", 200
```

A rule webhook's body describes the rule that fired and the events that matched it:

```javascript
const { ruleUuid, triggeredTimestampMs, deviceEvents = [] } = event;
for (const deviceEvent of deviceEvents) {
  console.log(ruleUuid, deviceEvent.deviceUuid, deviceEvent.activities, deviceEvent.timestampMs);
}
```

[Webhooks](https://developer.rhombus.com/webhooks#sample-webhook-payload) 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:

```mermaid
flowchart LR
    R[Rhombus] -- HTTPS POST --> F[Public receiver: verify and queue]
    F --> Q[(Managed queue)]
    W[Workers on your private network] -- outbound: poll --> Q
```

- 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](https://developer.rhombus.com/solutions/event-pipeline#reconcile-on-a-schedule).

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

1. **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**.

2. **Store the secret**

   Copy the webhook secret into your receiver's configuration, from a secrets manager rather than source code. [Find your secret](https://developer.rhombus.com/webhooks#find-your-secret) shows where each kind keeps it.

3. **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-`2xx` answers, signature failures and slow responses. A failed rule webhook delivery also appears in Rhombus as an `EXTERNAL_WEBHOOK_FAILURE` diagnostic 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.
