---
title: Notifications
description: Email digests and webhooks for conversations that need attention, including signature verification.
---

Hearl can tell your team as soon as a conversation needs attention. Set it up on the **Notifications** page. Only owners and admins can change these settings.

## When notifications are sent [#when]

Hearl checks every 15 minutes. A conversation is included when:

- its platform status is **No reply yet** or **User followed up**
- its progress is still **Not started**
- Hearl's AI judged it about your product, with no uncertain answers
- its priority is at or above the minimum you set: everything, Medium and up, or High only

Each conversation is notified once per status. If it was notified as **No reply yet** and the user later follows up, it's notified again as **User followed up**.

## Email [#email]

Turn on **Send email** and add recipients. Leave the list empty to email every workspace member. Any address works, such as a shared support inbox, up to 20 recipients.

Each check sends one digest that lists up to 10 conversations, with a link to the rest in the inbox. Choose English or Chinese under **Language**.

## Webhook [#webhook]

Enter an **Endpoint**. It must be a public `https` URL. Hearl recognizes common chat bots by their address and sends the message format they expect:

| Address | Sent as |
| --- | --- |
| `hooks.slack.com` | Slack message |
| `feishu.cn`, `larksuite.com` | Feishu text message |
| `qyapi.weixin.qq.com` | WeCom Markdown message |
| `oapi.dingtalk.com` | DingTalk Markdown message |
| `discord.com`, `discordapp.com` | Discord message |
| Any other URL | Generic JSON (below) |

The page shows which kind it detected. **Send a test** sends a sample, and the page shows the result of the last push. A request that takes longer than 8 seconds counts as a timeout.

### Generic JSON [#json]

```json
{
  "event": "conversations.needs_attention",
  "workspace": { "id": "org_123", "name": "Acme" },
  "text": "*2 conversations need attention* · Acme\n• [reddit] Sync stopped working after 2.3 — No reply yet <https://…>",
  "conversations": [
    {
      "id": "c_8f2a",
      "platform": "reddit",
      "product": "Acme",
      "title": "Sync stopped working after 2.3",
      "author": "some_user",
      "url": "https://www.reddit.com/r/…",
      "status": "待回复",
      "priority": "高",
      "category": "故障",
      "inbox_url": "https://hearl.org/app?c=c_8f2a"
    }
  ],
  "sent_at": "2026-11-08T09:15:00.000Z"
}
```

| Field | Description |
| --- | --- |
| `event` | `conversations.needs_attention`, or `test` for **Send a test**. |
| `workspace` | The workspace `id` and `name`. |
| `text` | A ready-to-post summary. Slack reads only this field. |
| `conversations` | Every conversation in this push. |
| `sent_at` | When the push was sent, in ISO 8601. |

`platform` is `x`, `reddit` or `hn`. `status`, `priority` and `category` are stable codes in Chinese, so your code can match on them whatever language your workspace uses:

| Field | Code | Meaning |
| --- | --- | --- |
| `status` | `待回复` | No reply yet |
| `status` | `用户有新回复` | User followed up |
| `priority` | `高` / `中` / `低` | High / Medium / Low |
| `category` | `故障` | Bug |
| `category` | `求助` | Question |
| `category` | `功能建议` | Feature request |
| `category` | `价格` | Pricing |
| `category` | `负面反馈` | Criticism |
| `category` | `正面反馈` | Praise |
| `category` | `购买咨询` | Buying |
| `category` | `其他` | Other |

### Verify the signature [#signature]

When you save a webhook endpoint, Hearl creates a **Signing secret** for it. You can show or rotate it on the Notifications page. The request carries this header:

```txt
X-Hearloop-Signature: sha256=<hex>
```

`<hex>` is the HMAC-SHA256 of the raw request body, keyed with your signing secret, as lowercase hex. Compute it over the exact bytes you received, before parsing the JSON, and compare in constant time.

```js Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.HEARL_WEBHOOK_SECRET;

app.post("/hearl", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + createHmac("sha256", secret).update(req.body).digest("hex");
  const received = req.get("x-hearloop-signature") ?? "";
  const ok =
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const payload = JSON.parse(req.body.toString("utf8"));
  console.log(payload.event, payload.conversations.length);
  res.status(200).end();
});

app.listen(3000);
```

```python Python
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["HEARL_WEBHOOK_SECRET"].encode()


@app.post("/hearl")
def hearl():
    body = request.get_data()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Hearloop-Signature", "")
    if not hmac.compare_digest(received, expected):
        abort(401)

    payload = json.loads(body)
    print(payload["event"], len(payload["conversations"]))
    return "", 200
```

Return any `2xx` status to acknowledge the push.
