Notifications
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
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.
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
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
{
"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://hearloop.zephyrwhimsy.workers.dev/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
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:
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.
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);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 "", 200Return any 2xx status to acknowledge the push.