---
title: Webhooks
---


## Introduction

Webhooks push data to your server automatically whenever new events occur—no polling required. Configure endpoints and manage delivery on the Webhooks page in the Dashboard, or browse payload schemas in the Event Reference.

{% button-group %}
{% button href="/docs/connect/webhooks/events" size="lg" %}Event Reference{% /button %}
{% button href="https://app.sahha.ai/dashboard/webhooks" variant="secondary" size="lg" %}Manage Webhooks{% /button %}
{% /button-group %}

{% image src="connect/webhooks/webhooks-dashboard.webp" alt="Webhooks Dashboard" width=768 height=300 / %}

---

## Event Types

| Event Type                         | Description                                                            | Schema                                          |
| ---------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------- |
| `ScoreCreatedIntegrationEvent`     | Health score generated (activity, sleep, wellbeing, readiness, etc.)   | [View](/docs/connect/webhooks/events#score)     |
| `BiomarkerCreatedIntegrationEvent` | Biomarker value calculated (steps, heart rate, sleep duration, etc.)   | [View](/docs/connect/webhooks/events#biomarker) |
| `ArchetypeCreatedIntegrationEvent` | Archetype assignment (activity level, sleep pattern, chronotype, etc.) | [View](/docs/connect/webhooks/events#archetype) |
| `DataLogReceivedIntegrationEvent`  | Raw health data received from user's device                            | [View](/docs/connect/webhooks/events#data-log)  |

{% button href="/docs/connect/webhooks/events" variant="default" %}View Event Reference{% /button %}

---

## Setup

1. Go to the [Sahha Dashboard](https://app.sahha.ai/)
2. Navigate to **Webhooks** in the sidebar
3. Add your endpoint URL (must be HTTPS)
4. Select which event types you want to receive
5. Copy your **secret key** for signature verification

{% image src="connect/webhooks/webhooks-secret.webp" alt="Webhooks Secret Key" width=500 / %}

{% callout title="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.
{% /callout %}

---

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

{% callout title="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.
{% /callout %}

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

{% tabs %}

{% tab label="JavaScript" %}

```javascript {% title="Webhook handler with signature verification" %}
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);
```

{% /tab %}

{% tab label="Python" %}

```python {% title="Webhook handler with signature verification" %}
from flask import Flask, request, jsonify
import hmac
import hashlib
import os

app = Flask(__name__)
SECRET_KEY = os.environ.get('SAHHA_WEBHOOK_SECRET')

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Signature')
    external_id = request.headers.get('X-External-Id')
    event_type = request.headers.get('X-Event-Type')

    # Validate headers
    if not all([signature, external_id, event_type]):
        return jsonify({'error': 'Missing required headers'}), 400

    # Verify signature
    payload = request.get_data()
    computed = hmac.new(
        SECRET_KEY.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature.lower(), computed.lower()):
        return jsonify({'error': 'Invalid signature'}), 401

    # Process event (do heavy work async to respond quickly)
    data = request.get_json()
    process_event_async(event_type, external_id, data)

    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)
```

{% /tab %}

{% tab label="C#" %}

```csharp {% title="Webhook handler with signature verification" %}
[ApiController]
[Route("[controller]")]
public class WebhookController : ControllerBase
{
    private readonly string _secretKey;

    public WebhookController(IConfiguration config)
    {
        _secretKey = config["Sahha:WebhookSecret"];
    }

    [HttpPost]
    public async Task<IActionResult> HandleWebhook()
    {
        var signature = Request.Headers["X-Signature"].ToString();
        var externalId = Request.Headers["X-External-Id"].ToString();
        var eventType = Request.Headers["X-Event-Type"].ToString();

        // Validate headers
        if (string.IsNullOrEmpty(signature) ||
            string.IsNullOrEmpty(externalId) ||
            string.IsNullOrEmpty(eventType))
        {
            return BadRequest(new { error = "Missing required headers" });
        }

        // Verify signature
        var payload = await new StreamReader(Request.Body).ReadToEndAsync();
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_secretKey));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        var computed = BitConverter.ToString(hash).Replace("-", "").ToLower();

        if (!signature.Equals(computed, StringComparison.OrdinalIgnoreCase))
        {
            return Unauthorized(new { error = "Invalid signature" });
        }

        // Process event (do heavy work async to respond quickly)
        _ = ProcessEventAsync(eventType, externalId, payload);

        return Ok(new { received = true });
    }
}
```

{% /tab %}

{% /tabs %}

---

## Best Practices

- **Always verify signatures** — Never process a webhook without validating `X-Signature` first
- **Respond quickly** — Return `200 OK` immediately, handle processing asynchronously
- **Handle duplicates** — Use the event `id` field 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](mailto:support@sahha.ai) or join the [Slack Community](https://join.slack.com/t/sahhacommunity/shared_invite/zt-1w0fmfbvk-qUwQ83tJgXyjT9XSxJvKIw).
