Webhooks
Get mentions and digests as signed requests to your own URL. Setup, retries, signature checks and every event.
A webhook is a channel that sends to a URL you control. Every alert rule attached to it sends one signed JSON POST for each matching mention (instant rules) or for each digest (hourly, daily and weekly rules).
- One URL can serve many rules. Use the payload's
eventto tell them apart. - A delivery tells you something happened. The mention itself is always at
GET /v1/mentions/{id}. - Reply with a 2xx right away and do the work afterwards. Rumoro ignores the response body.
Setup
- Create a webhook channel. The response contains the signing secret, which you only see once.
- Add alert rules that send to it, each with its own filter and
eventname. - Rumoro sends a
POSTfor every matching mention or digest. Reply with a 2xx within 10 seconds.
Create the channel
const { data: channel } = await rumoro.createChannel({
body: { kind: 'webhook', url: 'https://hooks.example.dev/rumoro', label: 'Support bot', headers: { 'X-Team': 'support' } },
});{
"id": "dest_...",
"label": "Support bot",
"stats": { "alerts": 0, "activeAlerts": 0, "lastDeliveryAt": null, "last7d": { "total": 0, "failed": 0 } },
"createdAt": "2026-10-04T09:12:44.208Z",
"kind": "webhook",
"config": {
"url": "https://hooks.example.dev/rumoro",
"headers": { "x-team": "support" },
"events": [],
"secret": "whsec_..."
}
}You get config.secret only in this response and after a rotation. Without a label, the URL's host is used. The URL must be public and use https.
Rumoro sends your headers with every request. You can set up to 20. Names are letters, digits and hyphens (stored lowercase), and values are up to 1,024 printable ASCII characters. Names that would clash with Rumoro's own signing or transport headers are refused with 400.
PATCH /v1/channels/{id} changes the URL, label or headers. Changing the URL or headers, or turning the channel off, cancels deliveries that haven't gone out yet.
Attach a rule
await rumoro.createAlert({
body: { name: 'Bug reports', mode: 'instant', event: 'mention.bug_report', filter: { intents: ['bug_report'] }, channelIds: ['dest_...'] },
});event can be any lowercase name with dots, up to 60 characters. Instant rules default to mention.matched and digest rules to digest.
Retries
When your URL doesn't reply with a 2xx within 10 seconds, Rumoro decides by the reason.
- Temporary problems are a timeout, a network error,
408,429or a5xx. Instant deliveries are tried up to 5 times, about 30 seconds apart, or later if yourRetry-Afterasks for it. Every attempt has the sameid. - Permanent problems are a redirect or any other
4xx. Rumoro doesn't retry and records the status in the delivery log. - Digests are retried per channel every 5 minutes for up to 6 hours. Hourly digests are retried until 45 minutes past the hour after they were due.
- A payload larger than 1 MiB is never sent.
Duplicates
A retry has the same id as the first attempt, so keep the ids you have processed and skip repeats. A replay (POST /v1/deliveries/{id}/replay) comes with a new id.
Each delivery is one keyword match. A post that matches two of your keywords arrives twice, with two mention ids and two keyword objects. Mentions marked ignored or done, and posts by muted authors, are not sent. Matches scored as noise only reach rules whose minRelevance is below 40.
Signature verification
Every request carries these headers.
| Header | Value |
|---|---|
Content-Type | application/json |
X-Mentions-Timestamp | When Rumoro built the request, in Unix seconds |
X-Mentions-Signature-V2 | v2= and the hex HMAC-SHA256 of the timestamp, a dot and the raw body |
X-Mentions-Signature | The hex HMAC-SHA256 of the raw body only. Kept for older code, without replay protection. |
Verify a request
Use the whole secret as the HMAC key, including whsec_. Check the V2 signature against the raw body before you parse it, compare in constant time, and reject timestamps more than five minutes from now. Because the timestamp is part of the signature, a captured request can't be reused later. Every retry gets a new timestamp and signature.
import { createHmac, timingSafeEqual } from 'node:crypto';
// body: the raw request body as received. secret: your channel secret, including "whsec_".
export function isFromRumoro(body: Buffer, timestamp: string | undefined, signature: string | undefined, secret: string) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || !signature || !(age <= 300)) return false;
const expected = Buffer.from('v2=' + createHmac('sha256', secret).update(timestamp + '.').update(body).digest('hex'));
const given = Buffer.from(signature);
return expected.length === given.length && timingSafeEqual(expected, given);
}
// isFromRumoro(rawBody, req.headers['x-mentions-timestamp'], req.headers['x-mentions-signature-v2'], secret)import hashlib
import hmac
import time
# body: the raw request body as bytes. secret: your channel secret, including "whsec_".
def is_from_rumoro(body: bytes, timestamp: str | None, signature: str | None, secret: str) -> bool:
if not timestamp or not signature or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > 300:
return False
digest = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest("v2=" + digest, signature)Rotate the secret
POST /v1/channels/{id}/rotate-secret creates a new secret and shows it once. The old secret stops working at the same moment, so update your code right away.
The payload
All events use the same envelope.
{
"id": "dlv_...",
"event": "mention.bug_report",
"createdAt": "2026-10-04T09:12:44.208Z",
"alert": { "id": "feed_...", "name": "Bug reports" },
"data": { }
}| Field | Meaning |
|---|---|
id | The delivery id. In the delivery log, scheduled daily and hourly digests are listed under the rule and period instead (feed_…:2026-10-04, feed_…:h:2026-10-04T09). |
event | The rule's event name, an account event, or test |
createdAt | When Rumoro built the request |
alert | The rule that sent it. alert.id is null for a channel test, and alert is null for account events. |
data | A mention, a digest or an account event. For a test it is { "message" }. |
Events
Rule events
The dashboard's Webhooks page offers these presets. Each one is a normal alert rule.
| Event | Mode | Filter |
|---|---|---|
mention.matched | instant | No filter, every relevant mention |
mention.high_relevance | instant | minRelevance: 80 |
mention.negative, mention.positive | instant | sentiments: ["negative"] or ["positive"] |
mention.buy_intent, mention.question, mention.complaint, mention.comparison, mention.churn_intent, mention.bug_report, mention.pricing | instant | intents with that intent |
review.negative | instant | ratings: [1, 2] |
digest | daily | No filter, one summary a day |
test | on request | Sent by the test calls below |
Account events
Account events don't use rules. A channel subscribes to them by name, with events on POST or PATCH /v1/channels.
| Event | Sent when |
|---|---|
keyword.capped | A keyword hit its monthly mention cap |
keyword.paused_for_balance | A keyword stopped because the balance ran out |
keyword.resumed | A keyword is collecting again |
wallet.low | The balance dropped to 20% of the last credit |
wallet.paused | All keywords stopped for lack of balance |
wallet.resumed | A credit restarted them |
mention.spike | A keyword got far more mentions than usual in the last hour |
sentiment.negative_spike | A keyword's mentions turned negative over the last day |
keyword.noisy | Most of a keyword's matches are noise |
channel.failing | A channel's recent deliveries all failed |
The last four come from Needs attention.
Testing and the delivery log
| Call | What it does |
|---|---|
POST /v1/channels/{id}/test | Sends an event: "test" request to this channel and returns { "outcomes": [{ "channelId", "ok", "error" }] } |
POST /v1/alerts/{id}/test | Does the same for every channel of the rule, with alert set |
GET /v1/channels/{id}/deliveries | Recent deliveries. Each has kind (mention, digest or event), event, status (pending, delivered or failed), attempts, the last error, sentAt, the rule's name and a short mention summary. Tests are not listed. |
POST /v1/deliveries/{id}/retry | Sends a failed or held delivery again with the same id |
POST /v1/deliveries/{id}/replay | Sends a finished delivery again with a new id |
DELETE /v1/channels/{id} | Removes the channel from all rules and returns 204 |
Retry and replay need a body with a requestId (a UUID), such as { "requestId": "2f0b7c1e-4d93-4a8e-9b61-5c3e7a0d1f24" }, and return 202. They are not in the API reference. Conventions explains how these endpoints answer.
With the CLI, use rumoro channels:test dest_... and rumoro channels:deliveries dest_.... The other calls are in the API reference.