Webhooks
Introduction
Webhooks push data to your server automatically whenever new events occur—no polling required. Receive scores, biomarkers, archetypes, insights, and raw data logs in real-time.
Setup
- Go to the Sahha Dashboard
- Navigate to Webhooks in the sidebar
- Add your endpoint URL (must be HTTPS)
- Select which event types you want to receive
- Copy your secret key for signature verification

Testing your endpoint
Use the Send Test Event button in the dashboard to verify your endpoint is receiving and processing webhooks correctly before going live.
Event Types
| Event Type | Description | Schema |
|---|---|---|
ScoreCreatedIntegrationEvent | Health score generated (activity, sleep, wellbeing, readiness, etc.) | View |
BiomarkerCreatedIntegrationEvent | Biomarker value calculated (steps, heart rate, sleep duration, etc.) | View |
ArchetypeCreatedIntegrationEvent | Archetype assignment (activity level, sleep pattern, chronotype, etc.) | View |
DataLogReceivedIntegrationEvent | Raw health data received from user's device | View |
Delivery
Interval
Scores and biomarkers update throughout the day as new data arrives. To control how frequently you receive these updates, configure the webhook interval in the dashboard.
The interval acts as a deduplication window. When a score or biomarker is delivered, subsequent updates for the same metric and profile are held until the interval expires. If multiple updates occur within the window, only the final value is sent when the interval ends.
| Interval | Behavior |
|---|---|
| Real-time | Every update delivered immediately |
| 1 min – 720 min | Updates batched; final value sent at interval end |
Data Logs are always real-time
The delivery interval only applies to scores and biomarkers. Data log events ( DataLogReceivedIntegrationEvent) are always delivered immediately since each log contains new raw data rather than an updated value.
Behavior
| Aspect | Details |
|---|---|
| Timeout | Sahha waits up to 30 seconds for a response |
| Success codes | 2xx status codes indicate successful delivery |
| Retries | Failed deliveries are retried up to 5 times with exponential backoff |
| Ordering | Events may arrive out of order—use timestamps for sequencing |
Handling Requests
Every webhook request includes these headers:
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 hash of the payload using your secret key |
X-External-Id | The profile's external identifier—use this to associate the event with a user in your system |
X-Event-Type | The event type string—use this to determine how to parse the payload |
Verifying Signatures
To ensure a webhook request is genuinely from Sahha (and not a malicious actor), verify the X-Signature header. Compute an HMAC-SHA256 hash of the raw request body using your secret key, then compare it to the signature in the header. If they match, the request is authentic.
Handler Example
const express = require('express');const crypto = require('crypto');
const app = express();app.use(express.text({ type: '*/*' }));
const SECRET_KEY = process.env.SAHHA_WEBHOOK_SECRET;
app.post('/webhook', (req, res) => { const signature = req.get('X-Signature'); const externalId = req.get('X-External-Id'); const eventType = req.get('X-Event-Type');
// Validate headers if (!signature || !externalId || !eventType) { return res.status(400).json({ error: 'Missing required headers' }); }
// Verify signature const hmac = crypto.createHmac('sha256', SECRET_KEY); const computed = hmac.update(req.body).digest('hex'); if (signature.toLowerCase() !== computed.toLowerCase()) { return res.status(401).json({ error: 'Invalid signature' }); }
// Process event (do heavy work async to respond quickly) const payload = JSON.parse(req.body); processEventAsync(eventType, externalId, payload);
res.status(200).json({ received: true });});
app.listen(3000); const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.text({ type: '*/*' }));
const SECRET_KEY = process.env.SAHHA_WEBHOOK_SECRET;
app.post('/webhook', (req, res) => {
const signature = req.get('X-Signature');
const externalId = req.get('X-External-Id');
const eventType = req.get('X-Event-Type');
// Validate headers
if (!signature || !externalId || !eventType) {
return res.status(400).json({ error: 'Missing required headers' });
}
// Verify signature
const hmac = crypto.createHmac('sha256', SECRET_KEY);
const computed = hmac.update(req.body).digest('hex');
if (signature.toLowerCase() !== computed.toLowerCase()) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process event (do heavy work async to respond quickly)
const payload = JSON.parse(req.body);
processEventAsync(eventType, externalId, payload);
res.status(200).json({ received: true });
});
app.listen(3000);
Best Practices
- Always verify signatures — Never process a webhook without validating
X-Signaturefirst - Respond quickly — Return
200 OKimmediately, handle processing asynchronously - Handle duplicates — Use the event
idfield for idempotency in case of retries - Use HTTPS — Your endpoint must use TLS encryption
- Secure your secret — Store in environment variables, never hardcode in your application
Support
Need help? Contact support@sahha.ai or join the Slack Community .