Updated 1 week ago Build

Creating Sahha Webhooks and Capturing Events

Set up an Express webhook endpoint with HMAC signature verification, expose it with ngrok, and test deliveries from the Sahha dashboard.

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 dotenv

Add a .env file

Create a file in the project root:

SAHHA_WEBHOOK_SECRET="PASTE_YOUR_SECRET_HERE"
PORT=3100
We use port 3100 here because many developers already have Next.js running on 3000.

Create 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.js

You should see:

Listening on http://localhost:3100

Your 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 project

After downloading:

  1. Unzip the project and open it in your editor
  2. Open the .env file and paste your webhook secret from the Sahha dashboard:
SAHHA_WEBHOOK_SECRET="PASTE_YOUR_SECRET_HERE"
PORT=3100
  1. Install dependencies and start the server:
npm install
node server.js

You should see:

Listening on http://localhost:3100

Your 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
ngrok gotcha: If you see 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.js must say Listening 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: