If you’re new to Sahha webhooks, start with the official webhooks documentation to understand how data delivery works.
By the end of this guide you will have a POST /sahha-webhook endpoint in Express with secure HMAC SHA-256 signature verification, a public HTTPS URL using ngrok, and a repeatable way to test webhook deliveries from the Sahha dashboard.
Prerequisites
Before you start, make sure you have:
- Node.js 18+ installed
- Access to Data Delivery > Webhooks in the Sahha dashboard
- ngrok installed (you must verify your email in ngrok)
Create the webhook endpoint (Express)
Create a new project
mkdir sahha-express-webhook
cd sahha-express-webhook
npm init -y
npm install express raw-body dotenvAdd a .env file
Create a file in the project root:
SAHHA_WEBHOOK_SECRET="PASTE_YOUR_SECRET_HERE"
PORT=3100Create server.js
Create server.js and paste the following:
import express from "express";
import getRawBody from "raw-body";
import crypto from "crypto";
import dotenv from "dotenv";
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
const SAHHA_SECRET = process.env.SAHHA_WEBHOOK_SECRET;
if (!SAHHA_SECRET) {
console.error("Missing SAHHA_WEBHOOK_SECRET in .env");
process.exit(1);
}
// IMPORTANT: Do NOT use express.json() or body-parser on this route.
// We must verify the signature against the raw request body.
app.post("/sahha-webhook", async (req, res) => {
try {
const signature = (req.header("X-Signature") || "").trim();
const eventType = (req.header("X-Event-Type") || "").trim();
const externalId = (req.header("X-External-Id") || "").trim();
if (!signature) {
return res.status(400).send("Missing X-Signature");
}
const rawBody = await getRawBody(req);
const computed = crypto
.createHmac("sha256", SAHHA_SECRET)
.update(rawBody)
.digest("hex");
if (computed !== signature) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(rawBody.toString("utf8"));
console.log("===== Sahha Webhook Received =====");
console.log("Event Type:", eventType || "(missing)");
console.log("External ID:", externalId || "(missing)");
console.log("Payload:", payload);
console.log("==================================");
return res.status(200).send("ok");
} catch (err) {
console.error("Webhook error:", err);
return res.status(500).send("server error");
}
});
app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}`);
});Run your server
node server.jsYou should see:
Listening on http://localhost:3100Your local endpoint is: http://localhost:3100/sahha-webhook
Download the starter project — it includes a working Express server with signature verification already set up.
Download starter projectAfter downloading:
- Unzip the project and open it in your editor
- Open the
.envfile and paste your webhook secret from the Sahha dashboard:
SAHHA_WEBHOOK_SECRET="PASTE_YOUR_SECRET_HERE"
PORT=3100- Install dependencies and start the server:
npm install
node server.jsYou should see:
Listening on http://localhost:3100Your local endpoint is: http://localhost:3100/sahha-webhook
Make your local server public with ngrok
Sahha cannot call localhost. You need a public HTTPS URL that forwards to your machine.
Run:
ngrok http 3100
You’ll see something like:
Forwarding https://xxxx-xxxx.ngrok-free.app -> http://localhost:3100
Your final webhook URL will be:
https://xxxx-xxxx.ngrok-free.app/sahha-webhook
authentication failed… email must be verified, go to your ngrok dashboard and verify your email, then re-run ngrok http 3100. Create the webhook in the Sahha dashboard
Go to Data Delivery > Webhooks > Create webhook and fill in:
- Webhook name: Local Express Webhook
- Destination URL:
https://xxxx-xxxx.ngrok-free.app/sahha-webhook - Secret: copy this from Sahha and paste into your
.env - Event subscriptions: choose the events you want (Scores, Biomarkers, Archetypes, Raw Data Logs)
Test delivery from Sahha
On your webhook list:
Click Actions → Test on your webhook
If everything is working you should see a 200 response in the Sahha dashboard and output like this in your terminal:
===== Sahha Webhook Received =====
Event Type: BiomarkerCreatedIntegrationEvent
External ID: webhook-test-id
Payload: {
id: '123e4567-e89b-12d3-a456-426614174000',
profileId: '123e4567-e89b-12d3-a456-426614174001',
accountId: '123e4567-e89b-12d3-a456-426614174002',
externalId: 'ext-789',
category: 'activity',
type: 'steps',
periodicity: 'daily',
aggregation: 'total',
value: '10000',
unit: 'count',
valueType: 'integer',
startDateTime: '2023-06-25T00:00:00+00:00',
endDateTime: '2023-06-25T23:59:59+00:00',
createdAtUtc: '2023-06-26T12:34:56+00:00',
version: 1
}
Troubleshooting
502 Bad Gateway (ngrok error page)
If the Sahha test returns 502 and you see ERR_NGROK_8012 — connection refused, it means ngrok is working but your Express server is not running (or is on the wrong port).
- Make sure your server is running —
node server.jsmust sayListening on 3100 - Make sure ngrok is forwarding the same port —
ngrok http 3100 - Make sure your Destination URL ends with
/sahha-webhook
401 Invalid signature
This usually means the secret in .env doesn’t match Sahha, you copied an old secret, or you restarted ngrok (URL changed) but didn’t update Sahha. Copy the current secret again from the dashboard, update .env, and restart your server.
Nothing appears in your server logs
- You clicked Test on the correct webhook
- The Destination URL matches the current ngrok URL
- Your server is running on the correct port
Next steps
You now have a working webhook receiver with correct signature verification and a reliable local testing setup. From here you can:
- Route by
X-Event-Typeto handle different event categories - Store events in a database
- Move to Sahha + Firebase (Cloud Functions) for production